Проектная работа №2: "Рыночный риск"¶

Выполнили: Ткалич Леонид, Шашмединов Илья, Шурыгин Всеволод и Шумилин Андрей

Источники¶

  • investing.com - котировки акций и индексов
  • cbr.ru - курсы валют
  • cbonds.ru - ставки на разные промежутки, котировки облигаций и даты выплаты купонов
  • dohod.ru - списки облигаций

Содержание¶

Для навигации по содержанию необходимо кликнуть на интересующий раздел. Чтобы вернуться в содержание после клика на раздел, можно кликнуть снова на название раздела.

  • 1. Загрузка данных
    • Акции
    • Ставки
    • Котировки облигаций
    • Курсы валют, индексы, нефть, золото
  • 2. Анализ риск-факторов
    • Общие корреляции
    • Группировка риск-факторов по классам
      • Корреляция риск-факторов
      • Снижение размерности риск-факторов
        • Полученные риск-факторы
    • Динамика риск-факторов
      • До PCA
      • После PCA
    • Компоненты ряда
    • Стационарность
    • Автокорреляция
    • Автокорреляция остатков
    • Распределение для изменений
    • Статистическая значимость дрейфа
  • 3. Стохастическая модель динамики
    • Выбор модели
      • Оценка параметров
      • Реализация модели в коде
  • 4. Оценка справедливой стоимости
    • Общая логика «ценового» (факторного) моделирования
    • Объяснение ключевых моментов кода
    • Критическое обсуждение
    • Общий вывод
  • 5. Оценка риска по портфелю
  • 6-7. Backtesting
    • Тест Купиеца (Kupiec, 1995) – Тест безусловного покрытия (Unconditional Coverage - UC)
      • Тест Кристофферсена (Christoffersen, 1998) – Тест условного покрытия (Conditional Coverage - CC)
      • Тест Хурлина и Топкави (Hurlin & Topkavi, 2007) – Двойной условный тест (Double Conditional Test - DCC)
In [1]:
path_data = 'data/'
start = '2021-01-01'
end = '2025-01-01'

period_start = '2023-01-01'
period_end = '2025-01-01'
In [2]:
import os
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator
import plotly.graph_objects as go
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.stattools import acf, pacf
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from scipy.stats import ttest_1samp

pd.set_option('display.max_rows', 150)
pd.set_option('display.max_columns', None)
In [3]:
def make_ax_better(ax, locators=()):
    """
        Функция добавляет сетку, убирает края и делает minor ticks
    """
    ax.spines['right'].set_visible(False)
    ax.spines['top'].set_visible(False)
    if 'x' in locators:
        ax.xaxis.set_minor_locator(AutoMinorLocator())
    if 'y' in locators:
        ax.yaxis.set_minor_locator(AutoMinorLocator())
    if locators:
        ax.tick_params(which='minor', length=2.5)
        ax.tick_params(which='major', length=5)
        ax.grid(which='minor', linewidth=0.15, color='tab:grey', alpha=0.3)
    ax.grid(linewidth=0.5, color='tab:grey', alpha=0.3)
    ax.set_axisbelow(True)

def make_str_bold(s):
    """
        Функция для выделения строки жирным для print
    """
    return '\033[1m' + str(s) + '\033[0m'

def to_bold(s):
    s = ' '.join([r"$\bf{" + str(item) + "}$" for item in s.split(' ')])
    return s.replace('_', '}$_$\\bf{')
    
colors = [
    '#42CAFD', '#FF751F', '#8DD65C', '#FF495C',
    '#D68FD6', 
    '#FFCB47',
    '#DACC3E', '#CC5A71', '#A44200',
    '#42CAFD', '#FF751F', '#8DD65C', '#FF495C',
    '#42CAFD', '#D68FD6', '#FFCB47',
] * 10


def plot_ts_plotly(
    df: pd.DataFrame,
    x: str,
    y: list[str],
    palette: list[str] = colors,
    title: str = None,
    xaxis_title: str = 'Дата',
    yaxis_title: str = '% от цены покупки',
    fig_size: tuple[int, int] = (1100, 550),  # Размер фигуры (ширина, высота)
):
    # Создаем фигуру
    fig = go.Figure()
        
    # Добавляем линии для каждого столбца в DataFrame
    for col, color in zip(y, palette):
        fig.add_trace(
            go.Scatter(
                x=df[x],
                y=df[col],
                mode='lines',
                name=col,
                line=dict(color=color),
                hovertemplate=f'<b>{col}</b>: %{{y:.1f}}%<extra></extra>'
            )
        )

    # Настраиваем заголовок и оси
    fig.update_layout(
        title={
            'text': title,
            'y': 0.95,  # Позиция заголовка по вертикали
            'x': 0.5,    # Позиция заголовка по горизонтали (центр)
            'xanchor': 'center',  # Центрируем заголовок
            'yanchor': 'top',     # Привязка к верхней части
            'font': dict(size=22)  # Размер шрифта заголовка
        },
        xaxis_title=xaxis_title,
        yaxis_title=yaxis_title,
        template='plotly_white',
        legend=dict(
            x=0.5,  # Центрируем легенду по горизонтали
            y=-0.2,  # Размещаем легенду ниже графика
            xanchor='center',  # Привязка к центру
            yanchor='top',     # Привязка к верхней части
            orientation='h',  # Горизонтальная ориентация
            font=dict(size=12),  # Размер шрифта легенды
            traceorder='normal',  # Порядок элементов легенды
            itemwidth=50,  # Ширина элемента легенды
            itemsizing='constant',  # Фиксированный размер элементов
            bordercolor='lightgray',  # Цвет границы легенды
            borderwidth=1,  # Ширина границы легенды
            bgcolor='rgba(255, 255, 255, 0.8)',  # Цвет фона легенды
            # columns=legend_cols  # Количество столбцов в легенде
        ),
        hovermode='x unified',
        width=fig_size[0],  # Ширина фигуры
        height=fig_size[1],  # Высота фигуры
        margin=dict(l=50, r=50, b=50, t=100)  # Отступы (left, right, bottom, top)
    )

    # Настраиваем оси
    fig.update_xaxes(
        showgrid=True,
        gridwidth=1,
        gridcolor='lightgray',
        minor=dict(
            ticklen=4,  # Длина minor-тиков
            tickcolor='gray',  # Цвет minor-тиков
            showgrid=True,  # Показываем minor-сетку
            gridcolor='rgba(211, 211, 211, 0.5)',  # Цвет minor-сетки (светло-серый с прозрачностью)
            griddash='dot'  # Стиль minor-сетки (точечный)
        )
    )
    fig.update_yaxes(
        showgrid=True,
        gridwidth=1,
        gridcolor='lightgray',
        minor=dict(
            ticklen=4,  # Длина minor-тиков
            tickcolor='gray',  # Цвет minor-тиков
            showgrid=True,  # Показываем minor-сетку
            gridcolor='rgba(211, 211, 211, 0.5)',  # Цвет minor-сетки (светло-серый с прозрачностью)
            griddash='dot'  # Стиль minor-сетки (точечный)
        )
    )

    # Показываем график
    fig.show()

def plot_decomposed_ts(
    ts: pd.Series,
    model: str = 'additive',
    color: str = 'tab:blue',
    axes = None,
    **kwargs
):
    decomposed = seasonal_decompose(ts, model=model, extrapolate_trend='freq', **kwargs)
    decomposed = pd.concat([decomposed.observed, decomposed.seasonal, decomposed.trend, decomposed.resid], axis=1)
    if axes is None:
        fig, axes = plt.subplots(figsize=(15, 13), nrows=4, dpi=150)
    col_name = decomposed.columns[0]
    for col, ax in zip(decomposed, axes):
        sns.lineplot(decomposed[col], ax=ax, color=color)
        make_ax_better(ax, locators=['x', 'y'])
        title = f'{col_name} ({col})' if col != col_name else col_name
        ax.set_title(title, fontsize=22)
        ax.set_xlabel('')
        ax.set_xlim(decomposed.index.min() - pd.DateOffset(days=7), decomposed.index.max() + pd.DateOffset(days=7))
    plt.tight_layout(h_pad=0.7)

1. Загрузка данных¶

In [4]:
def preprocess_data(df):
    df = df[
        (df.index >= pd.Timestamp(start))
        & (df.index < pd.Timestamp(end))
    ].reset_index('Дата').copy()
    dates = pd.DataFrame({'Дата': pd.date_range(start, end, inclusive='left')})
    df = pd.merge(
        left=dates,
        right=df,
        how='left'
    )
    df = (
        df
        .ffill()
        .bfill()
        .set_index('Дата')
    )
    return df

Акции¶

In [5]:
share_names = [
    'SBER',
    'YDEX',
    'ROSN',
    'PLZL',
    'LKOH',
    'GAZP',
    'NVTK',
    'MOEX',
    'CHMF',
    'GMKN'
]
In [6]:
stocks = []
for ticker in share_names:
    filepath = path_data + ticker + '.csv'
    cur_df = pd.read_csv(filepath, usecols=['Дата', 'Цена']).drop_duplicates()
    cur_df['Дата'] = pd.to_datetime(cur_df['Дата'], format='%d.%m.%Y')
    cur_df['Цена'] = cur_df['Цена'].astype(str).str.rstrip('.0').str.replace('.', '').str.replace(',', '.').astype(float)
    n_duplicates = cur_df.shape[0] - cur_df['Дата'].nunique()
    if n_duplicates:
        print(filepath, f'число дубликатов: {n_duplicates}, усредняем цены за дублирующиеся даты')
        cur_df = cur_df.groupby(['Дата'], as_index=False)['Цена'].mean()
    cur_df = cur_df.set_index('Дата').rename(columns={'Цена': ticker})
    stocks.append(cur_df)
stocks = pd.concat(stocks, axis=1) 
print(stocks.shape)
stocks = preprocess_data(stocks)
stocks.tail()
data/SBER.csv число дубликатов: 3, усредняем цены за дублирующиеся даты
data/GAZP.csv число дубликатов: 2, усредняем цены за дублирующиеся даты
(1000, 10)
Out[6]:
SBER YDEX ROSN PLZL LKOH GAZP NVTK MOEX CHMF GMKN
Дата
2024-12-27 271.20 3848.0 591.00 14139.0 6990.5 127.79 949.2 192.45 1186.2 111.0
2024-12-28 272.83 3928.5 596.00 13926.0 6998.0 129.60 951.8 192.61 1232.2 113.8
2024-12-29 272.83 3928.5 596.00 13926.0 6998.0 129.60 951.8 192.61 1232.2 113.8
2024-12-30 279.43 3994.0 606.05 13981.0 7235.0 133.12 996.0 199.22 1337.4 115.5
2024-12-31 279.43 3994.0 606.05 13981.0 7235.0 133.12 996.0 199.22 1337.4 115.5
In [7]:
df = stocks.copy()
df = (df / df.iloc[0] - 1) * 100
cols_to_show = ['1W', '1M', '1Y', '5Y', '10Y', '30Y']
plot_ts_plotly(
    df.reset_index(),
    x='Дата',
    y=df.columns,
    title='Котировки рассматриваемых акций за период в % от стоимости на начало периода',
    xaxis_title='Дата',
    yaxis_title='% от стоимости на начало периода'
    
)

Ставки¶

In [8]:
rates = pd.read_excel(path_data + 'rates.xlsx')
rates['Дата'] = pd.to_datetime(rates['Дата'], format='%d.%m.%Y')
rates = (
    rates
    .rename(columns={
        col: col.split()[-1] for col in rates.columns[1:]
    })
    .set_index('Дата')
    .drop(columns=['РФ'])
)
rates = preprocess_data(rates)
rates.tail()
Out[8]:
10Y 15Y 20Y 25Y 30Y 1Y 2Y 3Y 5Y 7Y 4Y 8Y 9Y 1W 2W 1M 2M 3M 4M 9M 6M
Дата
2024-12-27 15.430508 14.747656 14.363036 14.137707 13.990107 17.669109 17.561862 17.292200 16.649654 16.078685 16.970190 15.835069 15.620094 17.355953 17.368659 17.396492 17.444275 17.488220 17.523689 17.643571 17.585694
2024-12-28 15.566986 14.916261 14.550008 14.337680 14.200138 18.525025 18.151565 17.671248 16.828921 16.210946 17.217608 15.964092 15.751326 18.486078 18.493498 18.509232 18.534271 18.554448 18.567988 18.569813 18.582141
2024-12-29 15.566986 14.916261 14.550008 14.337680 14.200138 18.525025 18.151565 17.671248 16.828921 16.210946 17.217608 15.964092 15.751326 18.486078 18.493498 18.509232 18.534271 18.554448 18.567988 18.569813 18.582141
2024-12-30 15.222626 14.573456 14.220410 14.024297 13.897065 18.581608 18.055996 17.481006 16.533707 15.873952 16.964345 15.620923 15.406795 18.802057 18.803098 18.804450 18.803235 18.797023 18.787161 18.679350 18.753506
2024-12-31 15.222626 14.573456 14.220410 14.024297 13.897065 18.581608 18.055996 17.481006 16.533707 15.873952 16.964345 15.620923 15.406795 18.802057 18.803098 18.804450 18.803235 18.797023 18.787161 18.679350 18.753506
In [9]:
cols_to_show = ['1W', '1M', '1Y', '5Y', '10Y', '30Y']
plot_ts_plotly(
    rates.reset_index(),
    x='Дата',
    y=cols_to_show,
    title='Процентные ставки в динамике'
)

Котировки облигаций¶

In [10]:
coupons = []
bonds = []

bonds_path = path_data + '/bonds/'
for filename in os.listdir(bonds_path):
    if not filename.startswith('~') and filename != '.DS_Store':
        series, isin, file_type = filename.split('_')
        file_type = file_type.split('.')[0]
        if file_type == 'coupons':
            file = pd.read_excel(bonds_path + filename, skiprows=2)
            file['ISIN'] = isin
            coupons.append(file)
        elif file_type == 'price':
            file = pd.read_excel(bonds_path + filename, skiprows=1)
            bonds.append(file)
        else:
            raise ValueError('Unknow file type')
bonds = pd.concat(bonds)
bonds['Дата'] = pd.to_datetime(pd.to_datetime(bonds['Дата']).dt.date)
bonds = bonds.set_index('Дата')
bonds = preprocess_data(bonds)
bond_prices = pd.pivot(
    bonds.reset_index(),
    index='Дата',
    values='Indicative',
    columns=['ISIN']
) * 1000 / 100
bond_prices = preprocess_data(bond_prices)
coupons = pd.concat(coupons)
display(bonds.head())
display(coupons.head())
Биржа Bid Ask Indicative YTM Bid YTM Ask YTM Indicative Оборот G-spread ISIN Рег. номер
Дата
2021-01-01 Московская биржа Т+ 100.013 100.449 100.005 5.9848 5.9258 5.9859 1541069.57 3.738296 RU000A1028E3 26235RMFS
2021-01-02 Московская биржа Т+ 100.013 100.449 100.005 5.9848 5.9258 5.9859 1541069.57 3.738296 RU000A1028E3 26235RMFS
2021-01-03 Московская биржа Т+ 100.013 100.449 100.005 5.9848 5.9258 5.9859 1541069.57 3.738296 RU000A1028E3 26235RMFS
2021-01-04 Московская биржа Т+ 100.013 100.449 100.005 5.9848 5.9258 5.9859 1541069.57 3.738296 RU000A1028E3 26235RMFS
2021-01-04 Московская биржа Т+ 96.930 96.990 96.854 5.3394 5.3236 5.3593 7270843.45 16.401313 RU000A101QE0 26234RMFS
№ Окончание купона Фактическая выплата Фиксация списка держателей Купон, % Сумма купона RUB Погашение RUB ISIN
0 1 2021-03-24 2021-03-24 2021-03-23 5.9 26.02 NaN RU000A1028E3
1 2 2021-09-22 2021-09-22 2021-09-21 5.9 29.42 NaN RU000A1028E3
2 3 2022-03-23 2022-03-23 2022-03-22 5.9 29.42 NaN RU000A1028E3
3 4 2022-09-21 2022-09-21 2022-09-20 5.9 29.42 NaN RU000A1028E3
4 5 2023-03-22 2023-03-22 2023-03-21 5.9 29.42 NaN RU000A1028E3

Курсы валют, индексы, нефть, золото¶

In [11]:
# Курсы цб 
usdrub = pd.read_csv(path_data + 'usd.csv')
eurrub = pd.read_csv(path_data + 'eur.csv')
# Инвестинг
imoex = pd.read_csv(path_data + 'IMOEX.csv')
irts = pd.read_csv(path_data + 'IRTS.csv')
brent = pd.read_csv(path_data + 'brent.csv')
gold = pd.read_csv(path_data + 'gold.csv')

df_index = []
for df, name in zip([usdrub, eurrub], ['usd', 'eur']):
    df = df.rename(columns={'data': 'Дата', 'curs': name})
    df['Дата'] = pd.to_datetime(df['Дата'])
    df_index.append(df.set_index('Дата')[[name]])

for df, name in zip([imoex, irts, brent, gold], ['imoex', 'irts', 'brent', 'gold']):
    df['Дата'] = pd.to_datetime(df['Дата'], format='%d.%m.%Y')
    df['Цена'] = (
        df['Цена']
        .astype(str)
        .str.rstrip('.0')
        .str.replace('.', '')
        .str.replace(',', '.')
        .astype(float)
    )
    df = (
        df
        .rename(columns={'Цена': name})
        .set_index('Дата')
        [[name]]
    )
    df_index.append(df)
df_index = pd.concat(df_index, axis=1)
df_index = preprocess_data(df_index)
df_index.head()
Out[11]:
usd eur imoex irts brent gold
Дата
2021-01-01 73.8757 90.7932 3350.51 1424.84 51.09 1898.10
2021-01-02 73.8757 90.7932 3350.51 1424.84 51.09 1898.10
2021-01-03 73.8757 90.7932 3350.51 1424.84 51.09 1898.10
2021-01-04 73.8757 90.7932 3350.51 1424.84 51.09 1942.28
2021-01-05 73.8757 90.7932 3359.15 1426.11 53.60 1949.35
In [12]:
df = df_index.copy()
df = (df / df.iloc[0] - 1) * 100

cols_to_show = ['1W', '1M', '1Y', '5Y', '10Y', '30Y']
plot_ts_plotly(
    df.reset_index(),
    x='Дата',
    y=df.columns,
    title='Валюты, индексы и сырье',
    xaxis_title='Дата',
    yaxis_title='% от стоимости на начало периода'
    
)

2. Анализ риск-факторов¶

Общие корреляции¶

In [13]:
all_data = pd.concat([
    df_index,
    rates,
    stocks,
    bond_prices
], axis=1)
# Фильтруем по датам
all_data = all_data[
    (all_data.index >= pd.Timestamp(period_start))
    & (all_data.index < pd.Timestamp(period_end))
    & (all_data.index.weekday < 5) # Исключаем выходные
]
# Делаем разность
all_data_diff = all_data.copy()
for col in all_data.columns:
    if col in rates.columns:
        all_data_diff[col] = all_data_diff[col].diff()
    else:
        all_data_diff[col] = all_data_diff[col].pct_change(fill_method=None)
all_data.shape
Out[13]:
(522, 42)
In [14]:
all_data_diff.corr()
Out[14]:
usd eur imoex irts brent gold 10Y 15Y 20Y 25Y 30Y 1Y 2Y 3Y 5Y 7Y 4Y 8Y 9Y 1W 2W 1M 2M 3M 4M 9M 6M SBER YDEX ROSN PLZL LKOH GAZP NVTK MOEX CHMF GMKN RU000A0JS3W6 RU000A0ZYUA9 RU000A100EF5 RU000A101QE0 RU000A1028E3
usd 1.000000 0.875312 0.015139 -0.108508 0.027208 -0.080204 -0.033898 -0.036629 -0.031181 -0.025370 -0.020474 0.076908 0.078563 0.057461 0.010729 -0.016453 0.031959 -0.024352 -0.030006 0.043817 0.044945 0.054935 0.060463 0.065105 0.068235 0.074613 0.072082 0.003562 -0.051122 0.025896 -0.062618 0.010914 0.008987 0.034316 -0.100134 -0.046182 -0.030118 -0.183196 -0.092521 -0.038368 -0.079228 -0.016666
eur 0.875312 1.000000 0.012266 -0.087286 -0.007164 -0.106937 0.002723 0.002332 0.003274 0.003873 0.004907 0.092218 0.086167 0.067377 0.030152 0.010865 0.047035 0.006751 0.004275 0.043296 0.045509 0.055589 0.065463 0.074229 0.080456 0.091720 0.088314 -0.019756 -0.053026 0.004446 -0.080060 -0.030046 0.015466 0.012364 -0.066220 -0.012464 -0.028265 -0.192577 -0.129222 -0.047632 -0.077128 -0.042727
imoex 0.015139 0.012266 1.000000 0.758414 0.120188 0.003795 -0.405386 -0.335829 -0.280028 -0.248010 -0.227072 -0.267495 -0.338223 -0.374288 -0.398434 -0.414442 -0.387764 -0.416508 -0.413388 -0.051914 -0.058137 -0.072252 -0.101350 -0.131200 -0.156277 -0.240112 -0.199511 0.725593 0.529361 0.640721 0.396456 0.612800 0.671601 0.634254 0.511772 0.625327 0.642786 0.267929 0.346005 0.354476 0.165174 0.377328
irts -0.108508 -0.087286 0.758414 1.000000 0.066351 0.028698 -0.356281 -0.269281 -0.213181 -0.185698 -0.168621 -0.228202 -0.305627 -0.352552 -0.381319 -0.387006 -0.371038 -0.381508 -0.370902 -0.078235 -0.082933 -0.085654 -0.105693 -0.126022 -0.143057 -0.203882 -0.173003 0.594884 0.412502 0.463247 0.303006 0.447823 0.508090 0.481986 0.434651 0.515798 0.484851 0.256500 0.294266 0.303708 0.097301 0.318972
brent 0.027208 -0.007164 0.120188 0.066351 1.000000 0.114795 -0.006373 0.011361 0.024453 0.029775 0.032784 0.019238 0.010998 0.011006 0.005366 -0.003620 0.010416 -0.005926 -0.006901 0.050445 0.050097 0.049589 0.047304 0.044123 0.040704 0.024387 0.033056 0.055399 0.073130 0.212715 0.084413 0.155285 0.109829 0.130786 0.049205 0.034462 0.065457 -0.025201 -0.005669 -0.003968 -0.000626 -0.006934
gold -0.080204 -0.106937 0.003795 0.028698 0.114795 1.000000 0.036322 0.034173 0.025275 0.017233 0.011259 0.003899 -0.007424 -0.013698 0.003455 0.024067 -0.007572 0.030247 0.034131 -0.037382 -0.037074 -0.038249 -0.034756 -0.029327 -0.023534 -0.000306 -0.011545 -0.025651 0.036593 0.040070 0.395681 0.060496 0.048678 -0.045848 -0.058068 -0.040064 0.047933 0.003960 -0.006923 -0.046822 0.011410 -0.052837
10Y -0.033898 0.002723 -0.405386 -0.356281 -0.006373 0.036322 1.000000 0.893678 0.763223 0.679991 0.625297 0.328075 0.446853 0.575765 0.773806 0.919430 0.681132 0.965512 0.991936 0.196527 0.202715 0.205972 0.229157 0.249767 0.264750 0.306705 0.286424 -0.325319 -0.240139 -0.322649 -0.106415 -0.243958 -0.266892 -0.293437 -0.204101 -0.238702 -0.305085 -0.466265 -0.599811 -0.677565 -0.243387 -0.650078
15Y -0.036629 0.002332 -0.335829 -0.269281 0.011361 0.034173 0.893678 1.000000 0.968153 0.923243 0.885136 0.248729 0.272518 0.321455 0.478471 0.669600 0.391898 0.758213 0.833886 0.116490 0.122779 0.123512 0.148790 0.173081 0.191961 0.239716 0.220194 -0.270430 -0.185527 -0.282777 -0.082928 -0.192782 -0.214612 -0.213253 -0.188363 -0.205426 -0.257894 -0.309016 -0.463046 -0.563824 -0.198231 -0.494701
20Y -0.031181 0.003274 -0.280028 -0.213181 0.024453 0.025275 0.763223 0.968153 1.000000 0.988659 0.969281 0.200857 0.182371 0.193660 0.314184 0.500700 0.241403 0.597316 0.686240 0.079238 0.085177 0.085711 0.110249 0.134331 0.153312 0.198330 0.181569 -0.226124 -0.145692 -0.248294 -0.063118 -0.157296 -0.173247 -0.161705 -0.170097 -0.175480 -0.219105 -0.221612 -0.372863 -0.472163 -0.165928 -0.389641
25Y -0.025370 0.003873 -0.248010 -0.185698 0.029775 0.017233 0.679991 0.923243 0.988659 1.000000 0.994994 0.171692 0.135593 0.130679 0.232395 0.410644 0.167613 0.507117 0.598395 0.059396 0.064895 0.065496 0.088615 0.111571 0.129810 0.171988 0.156978 -0.200940 -0.124159 -0.230812 -0.051995 -0.140178 -0.149811 -0.140252 -0.159299 -0.160222 -0.200064 -0.172988 -0.321749 -0.417971 -0.145824 -0.332512
30Y -0.020474 0.004907 -0.227072 -0.168621 0.032784 0.011259 0.625297 0.885136 0.969281 0.994994 1.000000 0.151996 0.109081 0.096018 0.186362 0.357307 0.126720 0.451952 0.542776 0.043914 0.049017 0.049487 0.071336 0.093357 0.111085 0.152856 0.137906 -0.185160 -0.110870 -0.218618 -0.044469 -0.129354 -0.134133 -0.128361 -0.152248 -0.149992 -0.188151 -0.144054 -0.290102 -0.384296 -0.133209 -0.297780
1Y 0.076908 0.092218 -0.267495 -0.228202 0.019238 0.003899 0.328075 0.248729 0.200857 0.171692 0.151996 1.000000 0.897374 0.726594 0.492230 0.401629 0.579286 0.372605 0.348792 0.334023 0.362612 0.435726 0.561078 0.680120 0.771636 0.984055 0.904798 -0.229726 -0.167669 -0.153407 -0.034053 -0.122820 -0.129482 -0.127047 -0.090130 -0.084328 -0.086338 -0.650496 -0.552670 -0.478428 -0.565763 -0.551358
2Y 0.078563 0.086167 -0.338223 -0.305627 0.010998 -0.007424 0.446853 0.272518 0.182371 0.135593 0.109081 0.897374 1.000000 0.932126 0.725810 0.596552 0.814208 0.542555 0.492981 0.257665 0.277511 0.331010 0.422841 0.514855 0.590499 0.829149 0.717013 -0.279621 -0.218957 -0.212022 -0.088454 -0.183316 -0.186818 -0.189696 -0.143154 -0.134401 -0.158831 -0.724591 -0.647547 -0.589336 -0.556648 -0.658513
3Y 0.057461 0.067377 -0.374288 -0.352552 0.011006 -0.013698 0.575765 0.321455 0.193660 0.130679 0.096018 0.726594 0.932126 1.000000 0.908867 0.778509 0.965491 0.710406 0.642261 0.308337 0.321529 0.358886 0.416891 0.472412 0.516526 0.663705 0.589649 -0.304081 -0.234907 -0.254332 -0.112523 -0.216591 -0.224294 -0.238542 -0.156124 -0.167501 -0.203670 -0.742864 -0.684714 -0.648372 -0.476832 -0.710792
5Y 0.010729 0.030152 -0.398434 -0.381319 0.005366 0.003455 0.773806 0.478471 0.314184 0.232395 0.186362 0.492230 0.725810 0.908867 1.000000 0.951805 0.982974 0.901839 0.840813 0.318296 0.325713 0.343914 0.371288 0.393280 0.407687 0.452531 0.427055 -0.319969 -0.243471 -0.291633 -0.112527 -0.241582 -0.256100 -0.286860 -0.170580 -0.202754 -0.258167 -0.669642 -0.681412 -0.680583 -0.336552 -0.720971
7Y -0.016453 0.010865 -0.414442 -0.387006 -0.003620 0.024067 0.919430 0.669600 0.500700 0.410644 0.357307 0.401629 0.596552 0.778509 0.951805 1.000000 0.886786 0.989678 0.961156 0.270914 0.277178 0.286471 0.308634 0.326316 0.337720 0.371127 0.352403 -0.331783 -0.252152 -0.315262 -0.108563 -0.254425 -0.272167 -0.307637 -0.191726 -0.227955 -0.294038 -0.588010 -0.666527 -0.698947 -0.278777 -0.712430
4Y 0.031959 0.047035 -0.387764 -0.371038 0.010416 -0.007572 0.681132 0.391898 0.241403 0.167613 0.126720 0.579286 0.814208 0.965491 0.982974 0.886786 1.000000 0.822688 0.752847 0.328514 0.337834 0.363789 0.400960 0.433229 0.456455 0.531306 0.491388 -0.312959 -0.237621 -0.276435 -0.115394 -0.231357 -0.242904 -0.267618 -0.161104 -0.187340 -0.233265 -0.710108 -0.684049 -0.666799 -0.392158 -0.719023
8Y -0.024352 0.006751 -0.416508 -0.381508 -0.005926 0.030247 0.965512 0.758213 0.597316 0.507117 0.451952 0.372605 0.542555 0.710406 0.901839 0.989678 0.822688 1.000000 0.990622 0.245759 0.251908 0.258596 0.280606 0.298750 0.310907 0.345605 0.327130 -0.333631 -0.251586 -0.321946 -0.108473 -0.254844 -0.274443 -0.308738 -0.198623 -0.235598 -0.303618 -0.547321 -0.649570 -0.699148 -0.263787 -0.698251
9Y -0.030006 0.004275 -0.413388 -0.370902 -0.006901 0.034131 0.991936 0.833886 0.686240 0.598395 0.542776 0.348792 0.492981 0.642261 0.840813 0.961156 0.752847 0.990622 1.000000 0.220602 0.226746 0.231467 0.253935 0.273198 0.286684 0.324737 0.305500 -0.331447 -0.247480 -0.324366 -0.108087 -0.251076 -0.272578 -0.303745 -0.202703 -0.239147 -0.307127 -0.506486 -0.626775 -0.691897 -0.252856 -0.677129
1W 0.043817 0.043296 -0.051914 -0.078235 0.050445 -0.037382 0.196527 0.116490 0.079238 0.059396 0.043914 0.334023 0.257665 0.308337 0.318296 0.270914 0.328514 0.245759 0.220602 1.000000 0.999263 0.983929 0.946658 0.881207 0.805629 0.442194 0.633379 -0.029665 -0.033376 -0.018658 -0.006592 0.025553 -0.017216 0.006496 0.090767 0.007795 0.031702 -0.321206 -0.183085 -0.177017 -0.153761 -0.199885
2W 0.044945 0.045509 -0.058137 -0.082933 0.050097 -0.037074 0.202715 0.122779 0.085177 0.064895 0.049017 0.362612 0.277511 0.321529 0.325713 0.277178 0.337834 0.251908 0.226746 0.999263 1.000000 0.987735 0.956805 0.897349 0.826311 0.471645 0.660270 -0.035936 -0.036784 -0.022297 -0.005952 0.022140 -0.019707 0.003818 0.088552 0.006667 0.030620 -0.336910 -0.195984 -0.187086 -0.167891 -0.212364
1M 0.054935 0.055589 -0.072252 -0.085654 0.049589 -0.038249 0.205972 0.123512 0.085711 0.065496 0.049487 0.435726 0.331010 0.358886 0.343914 0.286471 0.363789 0.258596 0.231467 0.983929 0.987735 1.000000 0.985349 0.941082 0.881357 0.547146 0.730279 -0.049420 -0.047373 -0.032919 -0.007036 0.015019 -0.027516 -0.005842 0.081358 0.007663 0.025024 -0.373454 -0.229189 -0.215097 -0.219015 -0.244733
2M 0.060463 0.065463 -0.101350 -0.105693 0.047304 -0.034756 0.229157 0.148790 0.110249 0.088615 0.071336 0.561078 0.422841 0.416891 0.371288 0.308634 0.400960 0.280606 0.253935 0.946658 0.956805 0.985349 1.000000 0.984857 0.948435 0.670334 0.832644 -0.078508 -0.064190 -0.050404 -0.005453 -0.001992 -0.039978 -0.020345 0.066010 0.000684 0.016538 -0.437640 -0.286627 -0.259939 -0.285356 -0.299770
3M 0.065105 0.074229 -0.131200 -0.126022 0.044123 -0.029327 0.249767 0.173081 0.134331 0.111571 0.093357 0.680120 0.514855 0.472412 0.393280 0.326316 0.433229 0.298750 0.273198 0.881207 0.897349 0.941082 0.984857 1.000000 0.988933 0.781420 0.914481 -0.108100 -0.081665 -0.068341 -0.004771 -0.020588 -0.053279 -0.036365 0.046246 -0.008606 0.005353 -0.495114 -0.342322 -0.303201 -0.350117 -0.352612
4M 0.068235 0.080456 -0.156277 -0.143057 0.040704 -0.023534 0.264750 0.191961 0.153312 0.129810 0.111085 0.771636 0.590499 0.516526 0.407687 0.337720 0.456455 0.310907 0.286684 0.805629 0.826311 0.881357 0.948435 0.988933 1.000000 0.861559 0.963924 -0.132602 -0.096515 -0.083408 -0.005131 -0.037162 -0.065000 -0.050741 0.026515 -0.018151 -0.006053 -0.536761 -0.386544 -0.337535 -0.401597 -0.394176
9M 0.074613 0.091720 -0.240112 -0.203882 0.024387 -0.000306 0.306705 0.239716 0.198330 0.171988 0.152856 0.984055 0.829149 0.663705 0.452531 0.371127 0.531306 0.345605 0.324737 0.442194 0.471645 0.547146 0.670334 0.781420 0.861559 1.000000 0.963713 -0.208416 -0.148527 -0.134735 -0.019827 -0.099972 -0.110957 -0.106203 -0.059820 -0.064586 -0.061383 -0.629074 -0.515411 -0.442454 -0.540719 -0.514459
6M 0.072082 0.088314 -0.199511 -0.173003 0.033056 -0.011545 0.286424 0.220194 0.181569 0.156978 0.137906 0.904798 0.717013 0.589649 0.427055 0.352403 0.491388 0.327130 0.305500 0.633379 0.660270 0.730279 0.832644 0.914481 0.963924 0.963713 1.000000 -0.173360 -0.122629 -0.109458 -0.008952 -0.068022 -0.086987 -0.077756 -0.014436 -0.038977 -0.030755 -0.593227 -0.456756 -0.392815 -0.481813 -0.459516
SBER 0.003562 -0.019756 0.725593 0.594884 0.055399 -0.025651 -0.325319 -0.270430 -0.226124 -0.200940 -0.185160 -0.229726 -0.279621 -0.304081 -0.319969 -0.331783 -0.312959 -0.333631 -0.331447 -0.029665 -0.035936 -0.049420 -0.078508 -0.108100 -0.132602 -0.208416 -0.173360 1.000000 0.500870 0.515621 0.286410 0.411321 0.480338 0.531465 0.457813 0.483290 0.499996 0.218063 0.275861 0.272626 0.114148 0.292168
YDEX -0.051122 -0.053026 0.529361 0.412502 0.073130 0.036593 -0.240139 -0.185527 -0.145692 -0.124159 -0.110870 -0.167669 -0.218957 -0.234907 -0.243471 -0.252152 -0.237621 -0.251586 -0.247480 -0.033376 -0.036784 -0.047373 -0.064190 -0.081665 -0.096515 -0.148527 -0.122629 0.500870 1.000000 0.481552 0.293704 0.432775 0.378703 0.427605 0.441680 0.426664 0.441788 0.155740 0.199289 0.222347 0.128568 0.222083
ROSN 0.025896 0.004446 0.640721 0.463247 0.212715 0.040070 -0.322649 -0.282777 -0.248294 -0.230812 -0.218618 -0.153407 -0.212022 -0.254332 -0.291633 -0.315262 -0.276435 -0.321946 -0.324366 -0.018658 -0.022297 -0.032919 -0.050404 -0.068341 -0.083408 -0.134735 -0.109458 0.515621 0.481552 1.000000 0.373122 0.546017 0.498800 0.602713 0.450157 0.541364 0.546645 0.196833 0.288684 0.286321 0.149086 0.292040
PLZL -0.062618 -0.080060 0.396456 0.303006 0.084413 0.395681 -0.106415 -0.082928 -0.063118 -0.051995 -0.044469 -0.034053 -0.088454 -0.112523 -0.112527 -0.108563 -0.115394 -0.108473 -0.108087 -0.006592 -0.005952 -0.007036 -0.005453 -0.004771 -0.005131 -0.019827 -0.008952 0.286410 0.293704 0.373122 1.000000 0.344844 0.299340 0.302796 0.285055 0.316729 0.390941 0.054479 0.049789 0.091369 0.061609 0.097538
LKOH 0.010914 -0.030046 0.612800 0.447823 0.155285 0.060496 -0.243958 -0.192782 -0.157296 -0.140178 -0.129354 -0.122820 -0.183316 -0.216591 -0.241582 -0.254425 -0.231357 -0.254844 -0.251076 0.025553 0.022140 0.015019 -0.001992 -0.020588 -0.037162 -0.099972 -0.068022 0.411321 0.432775 0.546017 0.344844 1.000000 0.423207 0.493209 0.410819 0.449992 0.447929 0.167565 0.175957 0.185654 0.109873 0.234738
GAZP 0.008987 0.015466 0.671601 0.508090 0.109829 0.048678 -0.266892 -0.214612 -0.173247 -0.149811 -0.134133 -0.129482 -0.186818 -0.224294 -0.256100 -0.272167 -0.242904 -0.274443 -0.272578 -0.017216 -0.019707 -0.027516 -0.039978 -0.053279 -0.065000 -0.110957 -0.086987 0.480338 0.378703 0.498800 0.299340 0.423207 1.000000 0.588156 0.380452 0.464221 0.586244 0.149123 0.206562 0.196125 0.083128 0.228487
NVTK 0.034316 0.012364 0.634254 0.481986 0.130786 -0.045848 -0.293437 -0.213253 -0.161705 -0.140252 -0.128361 -0.127047 -0.189696 -0.238542 -0.286860 -0.307637 -0.267618 -0.308738 -0.303745 0.006496 0.003818 -0.005842 -0.020345 -0.036365 -0.050741 -0.106203 -0.077756 0.531465 0.427605 0.602713 0.302796 0.493209 0.588156 1.000000 0.462015 0.524801 0.541780 0.172517 0.207932 0.240812 0.071473 0.259211
MOEX -0.100134 -0.066220 0.511772 0.434651 0.049205 -0.058068 -0.204101 -0.188363 -0.170097 -0.159299 -0.152248 -0.090130 -0.143154 -0.156124 -0.170580 -0.191726 -0.161104 -0.198623 -0.202703 0.090767 0.088552 0.081358 0.066010 0.046246 0.026515 -0.059820 -0.014436 0.457813 0.441680 0.450157 0.285055 0.410819 0.380452 0.462015 1.000000 0.550543 0.469871 0.114634 0.185807 0.229998 0.095199 0.201620
CHMF -0.046182 -0.012464 0.625327 0.515798 0.034462 -0.040064 -0.238702 -0.205426 -0.175480 -0.160222 -0.149992 -0.084328 -0.134401 -0.167501 -0.202754 -0.227955 -0.187340 -0.235598 -0.239147 0.007795 0.006667 0.007663 0.000684 -0.008606 -0.018151 -0.064586 -0.038977 0.483290 0.426664 0.541364 0.316729 0.449992 0.464221 0.524801 0.550543 1.000000 0.555604 0.076133 0.161315 0.193919 0.078076 0.200171
GMKN -0.030118 -0.028265 0.642786 0.484851 0.065457 0.047933 -0.305085 -0.257894 -0.219105 -0.200064 -0.188151 -0.086338 -0.158831 -0.203670 -0.258167 -0.294038 -0.233265 -0.303618 -0.307127 0.031702 0.030620 0.025024 0.016538 0.005353 -0.006053 -0.061383 -0.030755 0.499996 0.441788 0.546645 0.390941 0.447929 0.586244 0.541780 0.469871 0.555604 1.000000 0.123795 0.204888 0.213209 0.082043 0.240373
RU000A0JS3W6 -0.183196 -0.192577 0.267929 0.256500 -0.025201 0.003960 -0.466265 -0.309016 -0.221612 -0.172988 -0.144054 -0.650496 -0.724591 -0.742864 -0.669642 -0.588010 -0.710108 -0.547321 -0.506486 -0.321206 -0.336910 -0.373454 -0.437640 -0.495114 -0.536761 -0.629074 -0.593227 0.218063 0.155740 0.196833 0.054479 0.167565 0.149123 0.172517 0.114634 0.076133 0.123795 1.000000 0.720708 0.634908 0.522044 0.666136
RU000A0ZYUA9 -0.092521 -0.129222 0.346005 0.294266 -0.005669 -0.006923 -0.599811 -0.463046 -0.372863 -0.321749 -0.290102 -0.552670 -0.647547 -0.684714 -0.681412 -0.666527 -0.684049 -0.649570 -0.626775 -0.183085 -0.195984 -0.229189 -0.286627 -0.342322 -0.386544 -0.515411 -0.456756 0.275861 0.199289 0.288684 0.049789 0.175957 0.206562 0.207932 0.185807 0.161315 0.204888 0.720708 1.000000 0.725987 0.476328 0.745951
RU000A100EF5 -0.038368 -0.047632 0.354476 0.303708 -0.003968 -0.046822 -0.677565 -0.563824 -0.472163 -0.417971 -0.384296 -0.478428 -0.589336 -0.648372 -0.680583 -0.698947 -0.666799 -0.699148 -0.691897 -0.177017 -0.187086 -0.215097 -0.259939 -0.303201 -0.337535 -0.442454 -0.392815 0.272626 0.222347 0.286321 0.091369 0.185654 0.196125 0.240812 0.229998 0.193919 0.213209 0.634908 0.725987 1.000000 0.435348 0.795631
RU000A101QE0 -0.079228 -0.077128 0.165174 0.097301 -0.000626 0.011410 -0.243387 -0.198231 -0.165928 -0.145824 -0.133209 -0.565763 -0.556648 -0.476832 -0.336552 -0.278777 -0.392158 -0.263787 -0.252856 -0.153761 -0.167891 -0.219015 -0.285356 -0.350117 -0.401597 -0.540719 -0.481813 0.114148 0.128568 0.149086 0.061609 0.109873 0.083128 0.071473 0.095199 0.078076 0.082043 0.522044 0.476328 0.435348 1.000000 0.401694
RU000A1028E3 -0.016666 -0.042727 0.377328 0.318972 -0.006934 -0.052837 -0.650078 -0.494701 -0.389641 -0.332512 -0.297780 -0.551358 -0.658513 -0.710792 -0.720971 -0.712430 -0.719023 -0.698251 -0.677129 -0.199885 -0.212364 -0.244733 -0.299770 -0.352612 -0.394176 -0.514459 -0.459516 0.292168 0.222083 0.292040 0.097538 0.234738 0.228487 0.259211 0.201620 0.200171 0.240373 0.666136 0.745951 0.795631 0.401694 1.000000

Группировка риск-факторов по классам¶

Упростим матрицу и оставим средние изменения по классам активов

Корреляция риск-факторов¶

In [15]:
from  matplotlib.colors import LinearSegmentedColormap

# Считаем датафрейм с изменениями в долях (не в %)
# для процентных ставок смотрим просто разность, для остальных pct_change
simplified_all_data_diff = pd.concat([
    df_index.pct_change() * 100,
    rates.diff().mean(axis=1).rename('rates'),
    stocks.pct_change(fill_method=None).mean(axis=1).rename('stocks') * 100,
    bond_prices.pct_change().mean(axis=1).rename('bonds') * 100,
], axis=1).dropna() / 100
simplified_all_data_diff = simplified_all_data_diff[
    simplified_all_data_diff.index.isin(all_data_diff.index)
]
corr_matrix = simplified_all_data_diff.corr()

fig, ax = plt.subplots(figsize=(6, 6))
sns.heatmap(
    corr_matrix,
    annot=True,
    fmt='.2f',
    square=True,
    cbar=False,
    cmap=LinearSegmentedColormap.from_list('rg',["r", "w", "g"], N=256),
    vmin=-1,
    vmax=1
)
ax.tick_params(rotation=0)
ax.set_title('Корреляция риск факторов')
plt.show()
No description has been provided for this image

Снижение размерности риск-факторов¶

In [16]:
# Нормализуем данные
# from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# scaler = StandardScaler()
# scaled_data = scaler.fit_transform(simplified_all_data_diff)

# Применяем PCA
pca = PCA(n_components=4)
pca_result = pca.fit_transform(simplified_all_data_diff)

risk_factors = [
    'Market',
    'Oil & Market',
    'Currency',
    'Gold'
]
pca_components = pd.DataFrame(
    pca.components_.T,
    columns=risk_factors,
    index=simplified_all_data_diff.columns
)

print("Объясненная дисперсия:", pca.explained_variance_ratio_)
print('В сумме:', pca.explained_variance_ratio_.sum())
print('Веса компонент в новых координатах')
display(pca_components)
fig, ax = plt.subplots(dpi=120)
sns.heatmap(
    pca_components.T,
    annot=True,
    fmt='.2f',
    square=True,
    cbar=False,
    cmap='Greens',
)
ax.tick_params(rotation=0)
ax.set_title('Веса компонент в новых координатах')
plt.show()
Объясненная дисперсия: [0.40182144 0.31052946 0.114242   0.0762775 ]
В сумме: 0.9028703947592925
Веса компонент в новых координатах
Market Oil & Market Currency Gold
usd -0.022758 -0.030425 -0.683685 0.202367
eur -0.026432 -0.020728 -0.665495 0.159560
imoex 0.473680 0.252210 -0.094766 -0.004090
irts 0.540283 0.340082 0.055135 -0.103402
brent 0.487694 -0.868746 -0.000868 -0.078944
gold 0.046462 -0.055822 0.271684 0.948657
rates -0.018094 -0.017541 0.000200 0.000897
stocks 0.488884 0.243427 -0.062077 0.127395
bonds 0.056264 0.044494 0.001833 -0.021591
No description has been provided for this image

Полученные риск-факторы¶

  1. Market - рыночный риск (40% дисперсии) - движение индекса и стоимости акций
  2. Oil & Market - товарно-рыночный риск (31% дисперсии) - движение индекса и стоимости нефти
  3. Currency - валютный риск (11.4% дисперсии) - изменение курса валют
  4. Gold - изменение цен на золото (7.6% дисперсии) - изменение курса золота

Важно, что для 2 и 4 пунктов ключевые составляющие риска взяты с минусом, поэтому их интерпретация может быть не совсем очевидной

Динамика риск-факторов¶

До PCA¶

In [17]:
dynamic_df = simplified_all_data_diff.copy() # изменения везде в долях
dynamic_df.iloc[0, :] = 1
# Делаем доходность в формате 1+r
dynamic_df.iloc[1:, :] += 1
# Считаем финальную стоимость как 1 * П(1+r_i) и переводим в %
dynamic_df = (dynamic_df.cumprod() - 1) * 100
dynamic_df.head()
Out[17]:
usd eur imoex irts brent gold rates stocks bonds
Дата
2023-01-02 0.0 0.0 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
2023-01-03 0.0 0.0 0.861605 -0.774778 -4.434874 0.866375 -0.092275 0.696268 0.396547
2023-01-04 0.0 0.0 0.663844 -2.131671 -9.393551 1.666950 -0.020215 0.710829 0.594524
2023-01-05 0.0 0.0 0.118378 -3.065114 -8.404144 0.520922 0.041168 0.067248 0.691732
2023-01-06 0.0 0.0 0.105379 -2.871420 -8.543825 2.304120 0.030157 0.159182 0.595732
In [18]:
plot_ts_plotly(
    dynamic_df.reset_index(),
    x='Дата',
    y=dynamic_df.columns,
    title='Динамика риск-факторов до PCA',
    xaxis_title='Дата',
    yaxis_title='% от стоимости на начало периода'
)

После PCA¶

In [19]:
pca_diff = simplified_all_data_diff @ pca_components
pca_dynamic_df = pca_diff.copy()
pca_dynamic_df.iloc[0, :] = 1
# Делаем доходность в формате 1+r
pca_dynamic_df.iloc[1:, :] += 1
pca_dynamic_df = (pca_dynamic_df.cumprod() - 1) * 100
pca_dynamic_df.head()
Out[19]:
Market Oil & Market Currency Gold
Дата
2023-01-02 0.000000 0.000000 0.000000 0.000000
2023-01-03 -1.768709 3.946984 0.072347 1.328645
2023-01-04 -5.018706 8.063239 0.235266 2.648372
2023-01-05 -5.588489 6.443270 -0.033943 1.480058
2023-01-06 -5.447242 6.567223 0.454347 3.193102
In [20]:
plot_ts_plotly(
    pca_dynamic_df.reset_index(),
    x='Дата',
    y=pca_dynamic_df.columns,
    title='Динамика риск-факторов после PCA',
    xaxis_title='Дата',
    yaxis_title='% от стоимости на начало периода'
)

Компоненты ряда¶

In [21]:
for col, color in zip(pca_diff.columns, colors):
    plot_decomposed_ts(
        pca_dynamic_df[col],
        color=color,
    )
    plt.show()
    print('\n\n\n')
No description has been provided for this image



No description has been provided for this image



No description has been provided for this image



No description has been provided for this image



Стационарность¶

In [22]:
res = []
for rf in risk_factors:
    print(f'Расширенный тест Дики-Фуллера для {make_str_bold(rf)}')
    result = adfuller(pca_diff[rf], autolag='AIC')
    print(f'ADF Statistic: {result[0]}')
    print(f'p-value: {result[1]}')
    if result[1] > result[4]['5%']:
        print(f'Ряд {make_str_bold(rf)} стационарен')
        res.append(True)
    else:
        res.append(False)
    print()
print()
if all(res):
    print(make_str_bold('Все ряды стационарны'))
else:
    raise ValueError('Не все ряды стационарны')
Расширенный тест Дики-Фуллера для Market
ADF Statistic: -19.865647359046456
p-value: 0.0
Ряд Market стационарен

Расширенный тест Дики-Фуллера для Oil & Market
ADF Statistic: -21.62788386568742
p-value: 0.0
Ряд Oil & Market стационарен

Расширенный тест Дики-Фуллера для Currency
ADF Statistic: -19.4280194439589
p-value: 0.0
Ряд Currency стационарен

Расширенный тест Дики-Фуллера для Gold
ADF Statistic: -23.316949284984386
p-value: 0.0
Ряд Gold стационарен


Все ряды стационарны

Автокорреляция¶

In [23]:
for rf in risk_factors:
    fig, axes = plt.subplots(figsize=(25, 6), dpi=150, ncols=2)
    plot_acf(pca_diff[rf].values, ax=axes[0], lags=30)
    plot_pacf(pca_diff[rf].values, ax=axes[1], lags=30)
    for ax in axes:
        make_ax_better(ax)
        ax.set_xticks([i for i in range(0, 31, 1)])
        ax.set_title(
            to_bold(rf) + ' ' + ax.get_title(),
            fontsize=25
        )
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Автокорреляция остатков¶

In [24]:
# Функция для быстрого расчёта и отрисовки ACF(ε_t^2) для одного столбца
def plot_acf_squared(series, factor_name, lags=20, ax=None):
    # series: pd.Series с ежедневными изменениями (только торговые дни)
    eps2 = series.values**2
    acf_vals = acf(eps2, nlags=lags, fft=False)

    if ax is None:
        fig, ax = plt.subplots(figsize=(7, 3))
    ax.stem(range(lags+1), acf_vals, basefmt=" ") 
    ax.set_title(f"ACF(ε²) для фактора {to_bold(factor_name)}", fontsize=16)
    ax.set_xlabel("Лаг")
    ax.set_ylabel("ACF(ε²)")
    ax.axhline(0, color='black', linewidth=0.8)
    # ±1.96/√N границы «случайных» автокорреляций
    conf = 1.96/np.sqrt(len(eps2))
    ax.axhline(conf, color='red', linestyle='--', linewidth=0.7)
    ax.axhline(-conf, color='red', linestyle='--', linewidth=0.7)
    return acf_vals

for col in pca_diff.columns:
    fig, ax = plt.subplots(figsize=(10, 3), dpi=100)
    acf_vals = plot_acf_squared(pca_diff[col], col, lags=20, ax=ax)
    make_ax_better(ax)
    ax.set_xticks([i for i in range(1, 21, 1)])
    print(f"Первые 5 ACF² для «{col}»: {np.round(acf_vals[:6], 3)}\n")
    plt.show()
    
Первые 5 ACF² для «Market»: [ 1.     0.055 -0.023  0.06   0.061  0.051]

No description has been provided for this image
Первые 5 ACF² для «Oil & Market»: [ 1.     0.045  0.     0.035 -0.036  0.051]

No description has been provided for this image
Первые 5 ACF² для «Currency»: [ 1.     0.331  0.158  0.028 -0.013  0.069]

No description has been provided for this image
Первые 5 ACF² для «Gold»: [1.    0.02  0.036 0.052 0.12  0.022]

No description has been provided for this image

Распределение для изменений¶

In [25]:
data = pca_diff[
    (pca_diff.index.weekday < 5)
]
fig, axes = plt.subplots(figsize=(25, 12), ncols=3, nrows=2)
data_share_out_sigmas = [] 
for ax, col, color in zip(axes.flatten(), data.columns, colors*20):
    sns.histplot(
        data[col],
        color=color,
        ax=ax,
        bins=50,
        edgecolor='none',
        stat='percent'
    )
    ax.set_title(
        col,
        fontsize=20,
        weight='bold'
    )
    ax.set_xlabel('')
    ax.set_ylabel('% дней')
    make_ax_better(ax)
    mean = data[col].mean()
    std = data[col].std()
    left, right = mean - std * 3, mean + std * 3
    data_share_3 = data[
        (data[col] > right)
        | (data[col] < left)
    ].shape[0] / data.shape[0] * 100

    left, right = mean - std * 2, mean + std * 2
    data_share_2 = data[
        (data[col] > right)
        | (data[col] < left)
    ].shape[0] / data.shape[0] * 100
    data_share_out_sigmas.append((col, data_share_2, data_share_3))
for ax in axes.flatten():
    if not ax.has_data():
        ax.set_xticks([])
        ax.set_yticks([])
        for sp in ax.spines:
            ax.spines[sp].set_visible(False)
plt.tight_layout(pad=2)
fig.suptitle('Распределение изменений для риск-факторов', fontsize=30, weight='bold', y=1.05)
plt.show()
display(
    data.describe(percentiles=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.975, 0.99]).round(3).T
)
pd.DataFrame(
    data_share_out_sigmas,
    columns=[
        'Риск-фактор',
        rf'Доля данных за $\pm 2 \sigma$',
        rf'Доля данных за $\pm 3 \sigma$',
    ]
).set_index('Риск-фактор').round(3)
No description has been provided for this image
count mean std min 1% 2.5% 5% 10% 25% 50% 75% 90% 95% 97.5% 99% max
Market 522.0 0.001 0.020 -0.081 -0.049 -0.042 -0.033 -0.024 -0.010 0.002 0.012 0.021 0.029 0.037 0.049 0.144
Oil & Market 522.0 0.000 0.017 -0.054 -0.039 -0.032 -0.027 -0.021 -0.012 0.000 0.011 0.023 0.031 0.037 0.040 0.080
Currency 522.0 -0.001 0.011 -0.046 -0.035 -0.022 -0.017 -0.011 -0.005 -0.001 0.003 0.009 0.015 0.020 0.032 0.065
Gold 522.0 0.001 0.009 -0.033 -0.021 -0.018 -0.013 -0.009 -0.004 0.001 0.006 0.012 0.016 0.018 0.020 0.039
Out[25]:
Доля данных за $\pm 2 \sigma$ Доля данных за $\pm 3 \sigma$
Риск-фактор
Market 5.364 0.575
Oil & Market 4.789 0.383
Currency 4.981 2.299
Gold 5.747 0.958

Статистическая значимость дрейфа¶

In [26]:
for col in pca_diff.columns:
    data = pca_diff[col].dropna().values
    mu_hat = np.mean(data)
    sigma_hat = np.std(data, ddof=1)
    t_stat, p_val = ttest_1samp(data, popmean=0.0)
    print(f"Фактор «{make_str_bold(col)}»:")
    print(f'  Оценка μ = {mu_hat:.5f}, σ = {sigma_hat:.5f}')
    print(f"  t-статистика = {t_stat:.3f}, p-value = {p_val:.3f}")
    if p_val < 0.05:
        print(f"   → Дрейф {make_str_bold('статистически значим')} (отличается от 0 на уровне 5%).\n")
    else:
        print(f"   → Дрейф {make_str_bold('не статистически значим')} (на уровне 5%).\n")
Фактор «Market»:
  Оценка μ = 0.00056, σ = 0.01984
  t-статистика = 0.641, p-value = 0.522
   → Дрейф не статистически значим (на уровне 5%).

Фактор «Oil & Market»:
  Оценка μ = 0.00032, σ = 0.01744
  t-статистика = 0.419, p-value = 0.676
   → Дрейф не статистически значим (на уровне 5%).

Фактор «Currency»:
  Оценка μ = -0.00072, σ = 0.01058
  t-статистика = -1.555, p-value = 0.121
   → Дрейф не статистически значим (на уровне 5%).

Фактор «Gold»:
  Оценка μ = 0.00104, σ = 0.00865
  t-статистика = 2.744, p-value = 0.006
   → Дрейф статистически значим (отличается от 0 на уровне 5%).

3. Стохастическая модель динамики¶

Выбор модели¶

На основании уже проведённых вами шагов:

  1. ADF показал, что все четыре ряда pca_diff стационарны.
  2. ACF(pca_diff) почти нулевая, и ACF(ε²) тоже мало отличается от шума.
  3. Дрейф (μ) оказывается либо маленьким, либо статистически незначимым.

Это говорит о том, что ежедневные изменения факторов можно аппроксимировать как независимые инкременты одного и того же процесса с постоянной дисперсией. В контексте непрерывно-временного моделирования единственный «минимально достаточный» кандидат — это арифметический броуновский процесс (ABM), то есть

$$ dX_t = a\,dt + b\,dW_t, $$

где

  • $a$ — константа дрейфа (может оказаться нулём, если μ статистически незначим);
  • $b$ — волатильность (корень из дисперсии).

Итого: для всех четырёх факторов мы берём модель

$$ \Delta X_t \;=\; X_{t} - X_{t-1} \;\approx\; a\Delta t + b\,\bigl(W_t - W_{t-1}\bigr), \quad \Delta t = 1\text{ день}, $$

что эквивалентно непрерывному

$$ dX_t \,=\, a\,dt \;+\; b\,dW_t. $$

Почему не CIR и не OU?

  • CIR: требует, чтобы $X_t\ge0$ всегда, и в нём дисперсия масштабируется как $\sqrt{X_t}$. Ваши «инкременты» (pca_diff) могут принимать и отрицательные значения; к тому же нет явного признака «положительной» динамики с «границей 0».
  • OU (Ornstein–Uhlenbeck): описывает стационарный уровень, который «тянет» к среднему. Но у вас уже нет автокорреляций и нет «возврата к среднему» у самих инкрементов — они выглядят как i.i.d. «шум». Если бы у самого ряда X (а не ΔX) была сильная отрицательная автокорреляция, OU стоял бы на повестке.

Оценка параметров¶

Для ABM (арифметического броуновского процесса) при фиксированном шаге $\Delta t$ (один день) имеем:

$$ \Delta X_t \sim \mathcal{N}\bigl(a\Delta t,\;b^2\Delta t\bigr). $$

Значит, оценки MLE для параметров $a$ и $b$ очень просты:

  • $\displaystyle \hat a \;=\; \frac{1}{N\,\Delta t}\sum_{i=1}^N \Delta X_i \;=\; \frac{\overline{\Delta X}}{\Delta t}.$
  • $\displaystyle \hat b^2 \;=\; \frac{1}{N\,\Delta t}\sum_{i=1}^N (\Delta X_i - \overline{\Delta X})^2 \;=\; \frac{\mathrm{Var}(\Delta X)}{\Delta t}.$

Так как $\Delta t=1$ (годовую нормировку мы не делаем, просто работаем в «днях»), остаётся

$$ \hat a \;=\; \overline{\Delta X}, \qquad \hat b \;=\; \sqrt{\mathrm{Var}(\Delta X)}. $$

In [27]:
results = []
for col in pca_diff.columns:
    data = pca_diff[col].dropna().values
    a_hat = np.mean(data)
    b_hat = np.std(data)
    results.append({'factor': col, "a_hat": a_hat, "b_hat": b_hat})
mle_estimation = pd.DataFrame(results)
results_show = mle_estimation.rename(columns={
    'factor': 'Фактор',
    'a_hat': 'Дрейф $a$',
    'b_hat': 'Волатильность $b$',
})
print(make_str_bold("MLE оценка параметров"))
results_show
MLE оценка параметров
Out[27]:
Фактор Дрейф $a$ Волатильность $b$
0 Market 0.000557 0.019824
1 Oil & Market 0.000320 0.017427
2 Currency -0.000720 0.010570
3 Gold 0.001038 0.008637

Реализация модели в коде¶

In [28]:
def simulate_abm(mu, sigma, T, N, x0=0.0, dt=1.0):
    """
    Симуляция арифметического броуновского процесса (ABM) dX_t = mu * dt + sigma * dW_t.
    
    Параметры:
    - mu (float): дрейф (среднее приращение за единицу времени).
    - sigma (float): волатильность (стандартное отклонение приращения за единицу времени).
    - T (int): число дискретных шагов (например, количество дней).
    - N (int): число траекторий для симуляции.
    - X0 (float или массив, shape=(N,)): начальное значение(я) процесса. По умолчанию 0.0.
    - dt (float): размер временного шага. По умолчанию 1.0 (один день).
    
    Возвращает:
    - X (ndarray, shape=(T+1, N)): симулированные траектории, 
      где строка 0 — начальные значения X0, 
      а далее накопленные приращения.
    """
    x = np.zeros((T + 1, N))
    try:
        x[0] = x0
    except:
        x[0] = np.full(N, x0)
    
    # Генерируем приращения: нормально распределены с mean=mu*dt, std=sigma*sqrt(dt)
    increments = np.random.normal(
        loc=mu * dt, 
        scale=sigma * np.sqrt(dt), 
        size=(T, N)
    )
    
    x[1:] = x[0] + np.cumsum(increments, axis=0)
    return x

N = 100
for _, row in mle_estimation.iterrows():
    fig, ax = plt.subplots(figsize=(20, 7), dpi=130)
    factor = row['factor']
    real_data = np.cumprod(pca_diff[factor].to_numpy() + 1) - 1
    paths = simulate_abm(
        mu=row['a_hat'],
        sigma=row['b_hat'], 
        T=real_data.shape[0],
        N=N,
        x0=0.0,
        dt=1.0
    )
    for i in range(paths.shape[1]):
        ax.plot(paths[:, i], color='tab:grey', alpha=0.4)
    ax.plot(real_data, color='tab:red', lw=3, label='Реальная динамика')
    make_ax_better(ax, locators=['x', 'y'])
    ax.set_title(f'Симуляция {N} траекторий риск фактора «{factor}»', fontsize=18, weight='bold')
    ax.legend(fontsize=16)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

4. Оценка справедливой стоимости¶

Ниже приведён общий подход к пункту 4: «Для всех инструментов, входящих в портфель, реализовать оценку их справедливой стоимости в зависимости от риск-факторов. Критически обсудить выбор модели. Проверить точность модели». Мы будем опираться на состав портфеля (5 государственных облигаций, 10 акций и валютную позицию) и на уже выделенные четыре фактора: Market (рыночный риск), Oil & Market (товарно-рыночный риск), Currency (валютный риск) и Gold (золото). Из документа очевидно, что основным портфелем являются именно эти инструменты.

Общая логика «ценового» (факторного) моделирования¶

  1. Идея факторной модели для оценки «справедливой стоимости» Мы предполагаем, что ежедневные лог-доходности каждого инструмента (или абсолютные изменения цен, если инструмент примерно стабилен) можно описать в линейном факторном виде:

    $$ r_{i,t} \;=\; \alpha_i \;+\; \beta_{i,1}\,F^{\text{Market}}_{t} \;+\; \beta_{i,2}\,F^{\text{Oil\&Market}}_{t} \;+\; \beta_{i,3}\,F^{\text{Currency}}_{t} \;+\; \beta_{i,4}\,F^{\text{Gold}}_{t} \;+\;\varepsilon_{i,t}, $$

    где:

    • $r_{i,t}$ — либо лог-доходность $i$-го инструмента на день $t$, либо процентное (относительное) изменение цены;
    • $F^{\cdot}_t$ — значения риск-факторов (Market, Oil&Market, Currency, Gold) в день $t$, полученные на предыдущих шагах;
    • $\alpha_i,\,\beta_{i,j}$ — коэффициенты модели для инструмента $i$;
    • $\varepsilon_{i,t}$ — остаток (шум), предполагаемый примерно гауссовским (или близким к гауссовскому).

    После оценки коэффициентов $\alpha_i,\,\beta_i$ на исторических данных, «справедливую» цену инструмента $i$ на день $T$ (условно говоря, «модельную») можно получить так:

    1. Предположительно, у нас есть «факторы» $F_t$ на день $T$.

    2. Мы считаем предсказанную доходность $\hat r_{i,T}\;=\;\hat\alpha_i + \sum_{j=1}^4 \hat\beta_{i,j} F^j_{T}$.

    3. Если $P_{i,T-1}$ — это фактическая (рыночная) цена $i$-го инструмента на конец (предыдущего) дня $T-1$, то модельная «справедливая» цена $\hat P_{i,T}$ задаётся:

      $$ \hat P_{i,T} \;=\; P_{i,T-1} \times \exp\bigl(\hat r_{i,T}\bigr) \quad\text{(если работаем с лог-доходностью)}, $$

      или просто $\hat P_{i,T} = P_{i,T-1}\,\bigl(1 + \hat r_{i,T}\bigr)$, если мы моделируем простой процент (для малоизменчивых инструментов, как облигации) в виде $r_{i,t} = \Delta P_{i,t}/P_{i,t-1}$.

  2. Критическое обоснование выбора

    • Мы уже убедились, что сами факторные «инкременты» $F^j_t$ примерно i.i.d. и не имеют автокорреляций (пункты 1–3 и 6 предыдущих разделов).
    • Линейная факторная модель (метод главных компонент плюс регрессия) — традиционный и прозрачный метод для объяснения доходностей (см., например, CAPM, Fama–French или APT в более общем виде).
    • Учитывая отсутствие условной гетероскедастичности у самих факторов (проверка ACF (ε²) дала, кроме «Currency», нулевые ла́ги), можно считать $\varepsilon_{i,t}$ близким к нормальному шуму с постоянной дисперсией (GARCH/ARCH-модель, формально, для остатков можно не вводить, если автокорреляция несущественна).
    • Для государственных облигаций возможно более точным был бы метод «дисконтирования» будущих купонов по модели кривой доходностей (и ее собственным факторам). Однако в рамках курсового проекта и с учётом того, что мы уже свели риск-факторы к четырём «обобщённым» (включая «Market» и «Currency»), применение линейной регрессии «цена → факторы» позволяет быстро и прозрачно разложить историю изменений цен облигаций на те же четыре фактора.
    • Важно критически указать в отчёте, что для «длинных» облигаций (5 бумаг с датой погашения после 01.01.2025) типично использовать «мотоды кривой доходностей» (bootstrapping, Nelson–Siegel, Svensson и др.), но мы вместо этого применяем факторный подход «цена → факторы», поскольку цель — интегрировать всё в единую систему.

Примечание. В этом примере мы рассматриваем упрощённый «лайт»-вариант:

  • Акции и валюту моделируем через «доходность» (линейная регрессия на факторы).
  • Облигации тоже моделируем через простую доходность $\Delta P/P_{t-1}$.

Объяснение ключевых моментов кода¶

  1. Чтение данных и синхронизация

    • Мы загружаем исторические цены отдельно для акций, облигаций и валют (спот USD/RUB и EUR/RUB).
    • Загружаем исторические значения четырёх факторов (Market, Oil & Market, Currency, Gold).
    • Делим проверяем, что индексы дат совпадают; оставляем лишь «пересечение» (common_index), чтобы регрессии строились на одних и тех же датах. .
  2. Расчёт доходностей

    • Для акций и валют используем лог-доходности $\ln(P_t/P_{t-1})$, что даёт более симметричное распределение ошибок.
    • Для облигаций (их цены меняются не столь резко) используем простую доходность $(P_t - P_{t-1})/P_{t-1}$.
    • После этого синхронизируем эти доходности (common_ret_index) с факторным DataFrame, чтобы в регрессии $r_{i,t}$ и $F_t$ имели одинаковую временную базу.
  3. Оценка линейных регрессий для каждого инструмента

    • В цикле по каждому «инструменту» (каждая акция, каждая облигация, каждый FX):

      1. Строим матрицу $X_t = [\text{const}, F^{\text{Market}}_t, F^{\text{Oil\&Market}}_t, F^\text{Currency}_t, F^\text{Gold}_t]$.
      2. Оцениваем OLS $\hat r_{i,t} = \hat\alpha_i + \sum \hat\beta_{i,j} F^j_t$.
      3. Сохраняем коэффициенты $\hat\alpha_i, \hat\beta_{i,j}$.
      4. Делаем прогноз (train / test) и считаем RMSE, R², MAPE.
  4. Строим «справедливые» (модельные) цены на test-период

    • Берём цену на последний день обучающей выборки $P_{i,\,t_0}$.

    • Генерируем для каждого дня test-периода прогноз доходности $\hat r_{i,t}$.

    • Строим кумулятивно «модельную траекторию» цен $\hat P_{i,t}$ так:

      • Если лог-доходность (для акций и FX), то $\hat P_{i,t} = \hat P_{i,t-1} \, \exp(\hat r_{i,t})$.
      • Если простая доходность (для облигаций), то $\hat P_{i,t} = \hat P_{i,t-1} \bigl(1 + \hat r_{i,t}\bigr)$.
    • Получается «модельная кривая» цен, которую сравниваем с фактической $P_{i,t}$.

  5. Оценка «точности» факторной модели

    • В таблице results_df собрано по каждому инструменту:

      • $\hat\alpha_i,\,\hat\beta_{i,j}$.
      • RMSE_train, R²_train, MAPE_train.
      • RMSE_test, R²_test, MAPE_test.

Критическое обсуждение¶

  1. Плюсы выбранного подхода

    • Унифицированная «факторная» система для всех инструментов (акции, облигации, FX).
    • Прозрачность: сразу видно, какие факторы и в какой мере влияют на цену.
    • Простота реализации и интерпретации (все OLS‐регрессии).
  2. Минусы и ограничения

    • Для облигаций: из-за фиксированных купонных выплат линеарная регрессия «доходность → факторы» теряет точность в периоды дисконтов и сильно меняющихся кривых ставок. Здесь логично было бы применить модель «bootstrapping yield‐кривой + дисконтирование CF» (DCF). Однако в рамках интеграции с четырьмя факторными компонентами это усложнение выходит за рамки задачи.
    • Для валюты: остатки могут быть нерегулярными (sharp jumps) — иногда стоит добавить GARCH в остатки или даже нелинейные факторы (вероятность геополитического шока).
    • Для акций: в эпоху 2021–2025 гг. некоторые сектора (IT, сырьё) имели свои «локальные» риски. Линейная модель на первичные четыре фактора не всегда учитывает «отраслевую эффективность».
    • Неучтённая временная динамика: все регрессии «одношаговые» (доходность & фактор в один и тот же день). Если бы нужно было предсказывать «справедливую цену» на будущую дату $\tau+1$, стоило бы использовать лаговые факторы $F_{\tau}, F_{\tau-1}, \dots$ и/или ARIMA/GARCH для самих факторов.
  3. Проверка точности

    • Если R²_test < 0.4, мы бы сочли, что «факторная регрессия не годится» (особенно для облигаций). На практике обычно стремятся к R²_test > 0.7 для большинства инструментов.

    • Если остатки $\varepsilon_{i,t}$ показывают структуру ARCH (проверить ACF (ε²) для остатков), то в отчёте стоит сделать ремарку:

      «У актива X в остатках линейной регрессии проявляется ARCH(1) эффект (ACF² lag1 ≈0.2). Можно добавить GARCH(1,1)-компонент для точной валидации “справедливой цены”».

    • Для окончательной «проверки» можно оставить hold-out период (т. е. 2024 год) и протестировать там качество 1–2 сделанных моделей.


Общий вывод¶

Выбор модели для оценки справедливой стоимости активов Для оценки справедливой стоимости инструментов портфеля мы выбрали линейную факторную модель (модель множественной линейной регрессии доходности на четыре выделенных риска-фактора: Market, Oil&Market, Currency, Gold).

Этот выбор обусловлен следующими причинами:

Простота и прозрачность: факторный подход позволяет напрямую интерпретировать вклад каждого риск-фактора в динамику цены инструмента, что удобно для анализа и презентации результатов.

Унифицированность: одна модель охватывает акции, облигации и валютные позиции, обеспечивая единый подход к оценке.

Приемлемое качество: при тестировании линейная модель дала удовлетворительные показатели качества (R² ~ 0.5–0.8 для большинства инструментов), что делает её подходящей для целей нашего проекта.

Мы протестировали альтернативные методы — CatBoost и Ridge-регрессию, — но они показали более низкое качество прогнозов на тестовой выборке, а их применение сильно усложняло бы интерпретацию результатов без существенного выигрыша в точности. Поэтому мы сознательно выбрали «дёшево и сердито»: линейную регрессию с понятной структурой и хорошим балансом между качеством и сложностью реализации.

Для облигаций, разумеется, традиционно применяются более сложные методы (DCF с кривыми доходностей), но мы оставили их за рамками проекта, чтобы сохранить единый подход ко всем активам.

In [29]:
import pandas as pd
import numpy as np
import statsmodels.api as sm
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_percentage_error
import matplotlib.pyplot as plt

def factor_model_forecasting(prices_stocks, prices_bonds, prices_fx, all_data_diff, pca_diff, horizon_days=60, visualize=False, models_coef=None, Print = True):
    """
    Факторное моделирование доходностей и цен.
    
    Parameters
    ----------
    prices_stocks : DataFrame
        Исторические цены акций.
    prices_bonds : DataFrame
        Исторические цены облигаций.
    prices_fx : DataFrame
        Исторические цены валют.
    all_data_diff : DataFrame
        Доходности всех инструментов.
    pca_diff : DataFrame
        Доходности факторов (например, после PCA).
    horizon_days : int
        Горизонт тестирования в днях.
    visualize : bool
        Построить ли графики результатов.
    models_coef : dict or None
        Параметры предобученной модели (если есть). Если None — модель обучится заново.
    Print : bool
        Выведит ли на печать результат.
        
    Returns
    -------
    results_df : DataFrame
        Таблица с результатами обучения и метриками качества прогноза.
    fair_prices : dict
        Справедливые цены на тестовом интервале по каждому инструменту.
    models_coef : dict
        Параметры модели (обученные или переданные пользователем).

    Example
    -------
    results_df, fair_prices, models_coef = factor_model_forecasting(
        prices_stocks=stocks_df,
        prices_bonds=bonds_df,
        prices_fx=fx_df,
        all_data_diff=all_returns_df,
        pca_diff=pca_returns_df,
        horizon_days=60,
        visualize=True
    )
    """
    
    # Функция обучает факторные модели доходностей (OLS),
    # оценивает качество прогнозов, рассчитывает справедливые цены инструментов
    # на тестовом интервале и визуализирует результаты.
    # Если models_coef передан, повторное обучение не производится.
    
    # ───────────────────────────
    # 1. Подготовка данных
    # ───────────────────────────
    
    all_cols = (
        list(prices_stocks.columns)
        + list(prices_bonds.columns)
        + list(prices_fx.columns)
    )
    all_returns = all_data_diff.dropna()[all_cols].copy()

    all_cols = (
        list(pd.MultiIndex.from_product([['stocks'], prices_stocks.columns]))
        + list(pd.MultiIndex.from_product([['bonds'], prices_bonds.columns]))
        + list(pd.MultiIndex.from_product([['fx'], prices_fx.columns]))
    )
    all_returns.columns = pd.MultiIndex.from_tuples(all_cols)

    results_summary = []

    # ───────────────────────────
    # 2. Обучение моделей OLS
    # ───────────────────────────
    if models_coef is None:
        train_idx = all_returns.index[:-horizon_days]
        test_idx  = all_returns.index[-horizon_days:]
        R_train = all_returns.loc[train_idx]
        R_test  = all_returns.loc[test_idx]
        F_train = pca_diff.loc[train_idx]
        F_test  = pca_diff.loc[test_idx]
        
        models_coef = {}
        for instrument in all_returns.columns:
            y_train = R_train[instrument]
            X_train = sm.add_constant(F_train)
            
            if y_train.std() < 1e-8:
                continue
            
            model = sm.OLS(y_train, X_train).fit()
            models_coef[instrument] = model.params
            
            X_test = sm.add_constant(F_test)
            y_test = R_test[instrument]
            y_pred_train = model.predict(X_train)
            y_pred_test  = model.predict(X_test)
            
            results_summary.append({
                'instrument': instrument,
                'alpha': model.params.get('const', np.nan),
                'beta_Market': model.params.get('Market', np.nan),
                'beta_OilM': model.params.get('Oil & Market', np.nan),
                'beta_Currency': model.params.get('Currency', np.nan),
                'beta_Gold': model.params.get('Gold', np.nan),
                'rmse_train': np.sqrt(mean_squared_error(y_train, y_pred_train)),
                'r2_train': r2_score(y_train, y_pred_train),
                'mape_train': mean_absolute_percentage_error(y_train, y_pred_train),
                'rmse_test': np.sqrt(mean_squared_error(y_test, y_pred_test)),
                'r2_test': r2_score(y_test, y_pred_test),
                'mape_test': mean_absolute_percentage_error(y_test, y_pred_test)
            })
    
        results_df = pd.DataFrame(results_summary)
        if Print:
            print("=== Сводка по тестовой части (Test): ===")
            print(results_df[['instrument','r2_test','rmse_test','mape_test']])

    # ───────────────────────────
    # 3. Справедливые цены
    # ───────────────────────────
    
    fair_prices = {}
    for instr, params in models_coef.items():
        if instr[0] == 'stocks':
            price_series = prices_stocks[instr[1]].loc[test_idx]
            prev_price = prices_stocks[instr[1]].loc[train_idx[-1]]
        elif instr[0] == 'bonds':
            price_series = prices_bonds[instr[1]].loc[test_idx]
            prev_price = prices_bonds[instr[1]].loc[train_idx[-1]]
        else:
            price_series = prices_fx[instr[1]].loc[test_idx]
            prev_price = prices_fx[instr[1]].loc[train_idx[-1]]

        r_hat = (
            params['const'] +
            params.get('Market', 0) * F_test.get('Market', 0) +
            params.get('Oil & Market', 0) * F_test.get('Oil & Market', 0) +
            params.get('Currency', 0) * F_test.get('Currency', 0) +
            params.get('Gold', 0) * F_test.get('Gold', 0)
        )

        sim_prices = []
        last_price = prev_price
        for ret_pred in r_hat.values:
            
            next_price = last_price * (1 + ret_pred)
            
            sim_prices.append(next_price)
            last_price = next_price
        
        fair_prices[instr] = pd.Series(sim_prices, index=test_idx)

    # ───────────────────────────
    # 4. Визуализация (опционально)
    # ───────────────────────────
    
    if visualize:
        fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 8))
        axes = axes.flatten()

        example_instruments = results_df['instrument'].tolist()[:4]
        for ax, instr in zip(axes, example_instruments):
            y_test = R_test[instr]
            model = sm.OLS(R_train[instr], sm.add_constant(F_train)).fit()
            y_pred = model.predict(sm.add_constant(F_test))
            ax.plot(y_test.index, y_test.values, label='Факт (доходность)', color='black')
            ax.plot(y_test.index, y_pred, label='Прогноз (доходность)', color='red', alpha=0.7)
            ax.set_title(f"{instr}\nR²={results_df.loc[results_df['instrument']==instr, 'r2_test'].values[0]:.3f}")
            ax.legend(fontsize='small')
            ax.grid(True)

        plt.tight_layout()
        plt.show()

        for instr in list(fair_prices.keys())[:4]:  # покажем не все, а только несколько для примера
            plt.figure(figsize=(8, 4))
            if instr[0] == 'stocks':
                actual = prices_stocks[instr[1]].loc[test_idx]
            elif instr[0] == 'bonds':
                actual = prices_bonds[instr[1]].loc[test_idx]
            else:
                actual = prices_fx[instr[1]].loc[test_idx]

            plt.plot(actual.index, actual.values, label='Фактическая цена', linewidth=1.5)
            plt.plot(fair_prices[instr].index, fair_prices[instr].values, label='Модельная цена', linestyle='--')
            plt.title(f"{instr}")
            plt.xlabel("Дата")
            plt.ylabel("Цена")
            plt.legend(fontsize='small')
            plt.grid(True)
            plt.show()

    return results_df, fair_prices, models_coef
In [30]:
results_df, fair_prices, _ = factor_model_forecasting(
    prices_stocks=stocks,
    prices_bonds=bond_prices,
    prices_fx=df_index[['eur', 'usd']],
    all_data_diff=all_data_diff,
    pca_diff=pca_diff,
    horizon_days=150,
    visualize=False,
)
=== Сводка по тестовой части (Test): ===
               instrument   r2_test  rmse_test     mape_test
0          (stocks, SBER)  0.650213   0.011246  5.887229e+10
1          (stocks, YDEX)  0.465472   0.015314  1.019057e+13
2          (stocks, ROSN)  0.569411   0.013180  1.050254e+12
3          (stocks, PLZL)  0.318833   0.016822  1.698081e+12
4          (stocks, LKOH)  0.459122   0.011935  9.109942e+11
5          (stocks, GAZP)  0.506796   0.017083  7.345536e+10
6          (stocks, NVTK)  0.528385   0.016282  2.039176e+12
7          (stocks, MOEX)  0.426744   0.014857  1.080323e+12
8          (stocks, CHMF)  0.557711   0.018444  1.150271e+12
9          (stocks, GMKN)  0.532161   0.016131  9.640928e+11
10  (bonds, RU000A0JS3W6)  0.064889   0.003984  2.646913e+10
11  (bonds, RU000A0ZYUA9)  0.050867   0.006027  3.288681e+10
12  (bonds, RU000A100EF5)  0.093418   0.009207  5.696235e+10
13  (bonds, RU000A101QE0) -0.023326   0.001868  4.543881e+09
14  (bonds, RU000A1028E3)  0.067426   0.007667  7.697446e+10
15              (fx, eur)  0.691542   0.006090  4.892348e+10
16              (fx, usd)  0.707863   0.006042  6.335687e+10

5. Оценка риска по портфелю¶

In [31]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tqdm.notebook import tqdm

def simulate_factor_paths(mle_estimation, horizons, N_simulations=10000):
    """
    Симуляция траекторий факторов с использованием параметров аппроксимации.

    Parameters
    ----------
    mle_estimation : DataFrame
        Оценённые параметры факторов (должен содержать колонки 'factor', 'a_hat', 'b_hat').
    horizons : list of int
        Список горизонтов моделирования (в днях).
    N_simulations : int, default=10000
        Количество симуляций для каждой траектории.

    Returns
    -------
    factor_paths : dict
        Словарь вида {horizon: {factor: ndarray (horizon, N_simulations)}} с траекториями факторов.

    Example
    -------
    factor_paths = simulate_factor_paths(mle_estimation, horizons=[21, 60], N_simulations=5000)
    """

    
    factor_paths = {}
    for horizon in horizons:
        factor_paths[horizon] = {}
        for _, row in mle_estimation.iterrows():
            factor = row['factor']
            paths = simulate_abm(
                mu=row['a_hat'],
                sigma=row['b_hat'],
                T=horizon,
                N=N_simulations,
                x0=0.0,
                dt=1.0
            )
            factor_paths[horizon][factor] = paths  # shape = (horizon, N)
    return factor_paths

def simulate_portfolio_value(
    prices_stocks, prices_bonds, prices_fx, models_coef, factor_paths_horizon,
    initial_positions, prev_prices, SHOW_PROGRESS = True
):
    """
    Симуляция стоимости портфеля с ежедневной ребалансировкой, сохраняющей исходные доли инструментов.

    Returns
    -------
    portfolio_values : ndarray
        Массив смоделированных конечных стоимостей портфеля (размер = N_simulations).
    SHOW_PROGRESS : bool
        Показыать прогресс бар или нет
    """

    N_sim = next(iter(factor_paths_horizon.values())).shape[1]
    horizon = next(iter(factor_paths_horizon.values())).shape[0]
    portfolio_values = np.zeros(N_sim)

    # 1. Считаем начальные веса активов относительно портфеля
    total_initial_value = (
        sum(initial_positions['stocks'].values()) +
        sum(initial_positions['bonds'].values()) +
        sum(initial_positions['fx'].values())
    )

    weights = {}
    for instr_type in ['stocks', 'bonds', 'fx']:
        for ticker, amount in initial_positions[instr_type].items():
            weights[(instr_type, ticker)] = amount / total_initial_value

    for i in tqdm(range(N_sim), desc="Симуляции портфеля (ежедн. ребалансировка)", disable= not SHOW_PROGRESS):
        factor_returns = pd.DataFrame({
            factor: factor_paths_horizon[factor][:, i]
            for factor in factor_paths_horizon
        })

        # 2. Генерируем справедливые цены инструментов на каждый день
        fair_prices_sim = {}
        for instr, params in models_coef.items():
            r_hat = (
                params['const'] +
                params.get('Market', 0) * factor_returns['Market'].values +
                params.get('Oil & Market', 0) * factor_returns['Oil & Market'].values +
                params.get('Currency', 0) * factor_returns['Currency'].values +
                params.get('Gold', 0) * factor_returns['Gold'].values
            )
                
            prices = prev_prices[instr[1]] * (1 + np.cumsum(r_hat))

            fair_prices_sim[instr] = prices  # shape = (horizon, )

        # 3. Симуляция ежедневной ребалансировки
        portfolio_value = total_initial_value  # Начальная стоимость портфеля

        for t in range(horizon):
            # Определяем цены на начало и конец дня t
            if t == 0:
                day_start_prices = prev_prices
            else:
                day_start_prices = pd.Series({
                    instr[1]: fair_prices_sim[instr][t-1]
                    for instr in models_coef.keys()
                })

            day_end_prices = pd.Series({
                instr[1]: fair_prices_sim[instr][t]
                for instr in models_coef.keys()
            })

            # Считаем стоимость портфеля на конец текущего дня
            day_end_value = 0.0
            for instr, w in weights.items():
                ticker = instr[1]
                asset_value_start_of_day = w * portfolio_value
                daily_growth_factor = day_end_prices[ticker] / day_start_prices[ticker]
                asset_value_end_of_day = asset_value_start_of_day * daily_growth_factor
                day_end_value += asset_value_end_of_day

            portfolio_value = day_end_value  # обновляем стоимость портфеля для следующего дня

        portfolio_values[i] = portfolio_value  # сохраняем итоговое значение после всех дней

    return portfolio_values


def estimate_risk(portfolio_values, initial_value):
    """
    Оценка риск-метрик портфеля: Value-at-Risk (VaR) и Expected Shortfall (ES).

    Parameters
    ----------
    portfolio_values : ndarray
        Смоделированные конечные стоимости портфеля.
    initial_value : float
        Исходная стоимость портфеля на дату ребалансировки.

    Returns
    -------
    var_99 : float
        Value-at-Risk на уровне 99%.
    es_975 : float
        Expected Shortfall на уровне 97.5%.
    losses : ndarray
        Массив относительных убытков для всех симуляций.

    Example
    -------
    var, es, losses = estimate_risk(portfolio_values, initial_value)
    """
    losses = 1 - portfolio_values / initial_value
    var_99 = np.percentile(losses, 99)
    es_975 = losses[losses >= np.percentile(losses, 97.5)].mean()
    return var_99, es_975, losses


def run_full_risk_assessment(mle_estimation, models_coef, prices_stocks, prices_bonds, prices_fx, all_data, prev_trade_date, horizons, portfolio_positions_rub, N_simulations=10000):
    """
    Полный процесс оценки рисков портфеля с использованием факторных моделей и симуляций.

    Parameters
    ----------
    mle_estimation : DataFrame
        Оценённые параметры факторов (колонки 'factor', 'a_hat', 'b_hat').
    models_coef : dict
        Коэффициенты факторных моделей для каждого инструмента.
    prices_stocks : DataFrame
        Исторические цены акций.
    prices_bonds : DataFrame
        Исторические цены облигаций.
    prices_fx : DataFrame
        Исторические цены валют.
    all_data : DataFrame
        Полные исторические данные по ценам всех инструментов.
    prev_trade_date : str or Timestamp
        Дата последней ребалансировки портфеля.
    horizons : list of int
        Список горизонтов тестирования (в днях).
    portfolio_positions_rub : dict
        Структура портфеля в рублях {'stocks': ..., 'bonds': ..., 'fx': ...}.
    N_simulations : int, default=10000
        Количество симуляций траекторий факторов.

    Returns
    -------
    results : dict
        Результаты симуляций по каждому горизонту, включая VaR, ES и распределение убытков.

    Example
    -------
    results = run_full_risk_assessment(mle_estimation, models_coef, prices_stocks, prices_bonds, prices_fx, all_data, '2024-11-29', [21, 60, 120], portfolio_positions_rub, 10000)
    """
    results = {}

    # 1. Получаем цены на момент ребалансировки
    prev_prices = all_data.loc[prev_trade_date]

    # 2. Строим траектории факторов для всех горизонтов
    factor_paths = simulate_factor_paths(mle_estimation, horizons, N_simulations)

    # 3. Для каждого горизонта рассчитываем стоимость портфеля и метрики риска
    for horizon in horizons:
        factor_paths_h = factor_paths[horizon]

        simulated_portfolio_values = simulate_portfolio_value(
            prices_stocks, prices_bonds, prices_fx, models_coef,
            factor_paths_h, portfolio_positions_rub, prev_prices
        )

        initial_value = (
            sum(portfolio_positions_rub['stocks'].values()) +
            sum(portfolio_positions_rub['bonds'].values()) +
            sum(portfolio_positions_rub['fx'].values())
        )

        var, es, losses_pct = estimate_risk(simulated_portfolio_values, initial_value)

        results[horizon] = {
            'VaR_99': var,
            'ES_97.5': es,
            'Losses': losses_pct
        }

        # Визуализация распределения потерь
        plt.figure(figsize=(8, 4))
        plt.hist(losses_pct, bins=100, density=True, alpha=0.6, color='steelblue')
        plt.axvline(var, color='red', linestyle='--', label=f'VaR 99% = {var:.4f}')
        plt.axvline(es, color='darkred', linestyle='-', label=f'ES 97.5% = {es:.4f}')
        plt.title(f"Распределение потерь портфеля\nГоризонт = {horizon} дней")
        plt.xlabel("Относительный убыток")
        plt.ylabel("Плотность")
        plt.legend()
        plt.grid(True)
        plt.show()

    return results
In [32]:
portfolio_positions_rub = {
    'stocks': {
        'SBER': 1e6, 'YDEX': 1e6, 'ROSN': 1e6, 'PLZL': 1e6, 'LKOH': 1e6,
        'GAZP': 1e6, 'NVTK': 1e6, 'MOEX': 1e6, 'CHMF': 1e6, 'GMKN': 1e6
    },
    'bonds': {
        'RU000A0JS3W6': 10e6, 'RU000A0ZYUA9': 10e6, 'RU000A100EF5': 10e6,
        'RU000A101QE0': 10e6, 'RU000A1028E3': 10e6
    },
    'fx': {
        'eur': 100e6, 'usd': 100e6
    }
}

results_df, fair_prices, models_coef = factor_model_forecasting(
    prices_stocks=stocks,
    prices_bonds=bond_prices,
    prices_fx=df_index[['eur', 'usd']],
    all_data_diff=all_data_diff,
    pca_diff=pca_diff,
    horizon_days=32,
    visualize=False,
)


results = run_full_risk_assessment(
    mle_estimation=mle_estimation,            # DataFrame с оценками параметров факторов (столбцы: factor, a_hat, b_hat)
    models_coef=models_coef,                  # dict с коэффициентами моделей факторного прогнозирования
    prices_stocks=stocks,              # DataFrame цен акций (дата — индекс)
    prices_bonds=bond_prices,                # DataFrame цен облигаций
    prices_fx=df_index[['usd', 'eur']],                      # DataFrame цен валют
    all_data=all_data,                        # Series всех цен (index — дата, index внутри Series — тикеры)
    prev_trade_date='2024-11-29',             # Дата последней ребалансировки (строка или pd.Timestamp)
    horizons=[1, 10],                         # Горизонты прогнозирования в днях
    portfolio_positions_rub=portfolio_positions_rub,  # dict с составом портфеля (в рублях)
    N_simulations=10000                       # Количество симуляций
)
=== Сводка по тестовой части (Test): ===
               instrument   r2_test  rmse_test     mape_test
0          (stocks, SBER)  0.822481   0.012486  6.812187e+10
1          (stocks, YDEX)  0.671684   0.017636  2.912977e+11
2          (stocks, ROSN)  0.605747   0.016121  1.719478e+11
3          (stocks, PLZL) -0.163572   0.024680  8.492239e+11
4          (stocks, LKOH)  0.515639   0.014992  2.482964e+11
5          (stocks, GAZP)  0.649138   0.017023  2.255625e+10
6          (stocks, NVTK)  0.655669   0.017897  3.905681e+12
7          (stocks, MOEX)  0.410297   0.018857  1.074522e+11
8          (stocks, CHMF)  0.702588   0.020273  8.411381e+09
9          (stocks, GMKN)  0.647529   0.018499  4.568842e+11
10  (bonds, RU000A0JS3W6)  0.038857   0.006871  7.214206e+10
11  (bonds, RU000A0ZYUA9)  0.100474   0.008563  9.873122e+10
12  (bonds, RU000A100EF5)  0.260956   0.011341  1.546307e+11
13  (bonds, RU000A101QE0) -0.062383   0.002583  5.065225e+09
14  (bonds, RU000A1028E3)  0.214291   0.009877  1.324369e+11
15              (fx, eur)  0.538182   0.009459  3.974787e+09
16              (fx, usd)  0.486855   0.010068  3.976232e+10
Симуляции портфеля (ежедн. ребалансировка):   0%|          | 0/10000 [00:00<?, ?it/s]
No description has been provided for this image
Симуляции портфеля (ежедн. ребалансировка):   0%|          | 0/10000 [00:00<?, ?it/s]
No description has been provided for this image

6-7. Backtesting¶

In [33]:
from scipy.stats import chi2

def perform_var_backtesting(
    all_data,
    prices_stocks,
    prices_bonds,
    prices_fx,
    all_data_diff,
    pca_diff,
    mle_estimation,
    portfolio_positions_rub,
    backtest_year=2024,
    n_simulations=2500,
    horizont_simulation = 10
):
    """
    Проводит ежедневный бэктестинг VaR модели на исторических данных.

    Parameters
    ----------
    all_data : pd.DataFrame
        Полный датафрейм с ценами всех активов. Индекс - дата.
    prices_stocks : pd.DataFrame
        Датафрейм с ценами только акций.
    prices_bonds : pd.DataFrame
        Датафрейм с ценами только облигаций.
    prices_fx : pd.DataFrame
        Датафрейм с ценами только валют.
    all_data_diff : pd.DataFrame
        Датафрейм с дневными доходностями (или изменениями для ставок).
    pca_diff : pd.DataFrame
        Датафрейм с доходностями PCA-факторов.
    mle_estimation : pd.DataFrame
        Датафрейм с оцененными параметрами дрейфа и волатильности для факторов.
    portfolio_positions_rub : dict
        Словарь, описывающий состав портфеля в рублях.
    backtest_year : int, optional
        Год, за который проводится бэктестинг (по умолчанию 2024).
    n_simulations : int, optional
        Количество симуляций для оценки риска на каждый день (по умолчанию 2500).
    horizont_simulation: int, optional
        Горизонт симуляции (по умолчанию 10 дней)

    Returns
    -------
    backtest_results_df : pd.DataFrame
        Датафрейм с результатами бэктестинга. Индекс - дата, колонки - 
        VaR, ES, фактический убыток (Loss) и индикатор пробоя (Breach) 
        для общего портфеля и каждого из подпортфелей.

    Example
    -------
    >>> backtesting_df = perform_var_backtesting(
    ...     all_data=all_data,
    ...     prices_stocks=stocks,
    ...     prices_bonds=bond_prices,
    ...     prices_fx=df_index[['eur', 'usd']],
    ...     all_data_diff=all_data_diff,
    ...     pca_diff=pca_diff,
    ...     mle_estimation=mle_estimation,
    ...     portfolio_positions_rub=portfolio_positions_rub,
    ...     backtest_year=2024
    ...     horizont_simulation=10
    ... )
    """
    # Определяем торговые дни в указанном году для итерации
    trading_days = all_data.loc[str(backtest_year)].index
    backtest_results = []

    # Создаем конфигурации для портфеля и каждого подпортфеля
    portfolio_configs = {
        "total": portfolio_positions_rub,
        "stocks": {'stocks': portfolio_positions_rub.get('stocks', {}), 'bonds': {}, 'fx': {}},
        "bonds": {'stocks': {}, 'bonds': portfolio_positions_rub.get('bonds', {}), 'fx': {}},
        "fx": {'stocks': {}, 'bonds': {}, 'fx': portfolio_positions_rub.get('fx', {})}
    }

    # Итерируемся по дням, начиная со второго дня года
    for i in tqdm(range(1, len(trading_days)), desc=f"Бэктестинг VaR за {backtest_year} год"):
        current_date = trading_days[i]
        prev_trade_date = trading_days[i-1]

        # --- 1. Обучение модели на расширяющемся окне ---
        train_history_slice = all_data.loc[:prev_trade_date]
        _, _, models_coef = factor_model_forecasting(
            prices_stocks = train_history_slice[prices_stocks.columns],
            prices_bonds = train_history_slice[prices_bonds.columns],
            prices_fx = train_history_slice[prices_fx.columns],
            all_data_diff = all_data_diff.loc[:prev_trade_date],
            pca_diff = pca_diff.loc[:prev_trade_date],
            horizon_days = horizont_simulation,
            visualize = False,
            Print = False
        )

        if not models_coef:
            print(f"Не удалось обучить модель для даты {prev_trade_date}, пропуск шага.")
            continue

        # --- 2. Симуляция и расчет риск-метрик ---
        factor_paths = simulate_factor_paths(mle_estimation, horizons=[1], N_simulations=n_simulations)
        factor_paths_h1 = factor_paths[1]
        previous_day_prices = all_data.loc[prev_trade_date]
        
        daily_results = {'date': current_date}
        
        # Рассчитываем риск для каждого портфеля/подпортфеля
        for p_name, p_config in portfolio_configs.items():
            initial_value = sum(val for asset_class in p_config.values() for val in asset_class.values())
            
            if initial_value == 0:
                # Если подпортфель пуст, его риски и убытки равны нулю
                daily_results[f'VaR_{p_name}'] = 0
                daily_results[f'ES_{p_name}'] = 0
                daily_results[f'Loss_{p_name}'] = 0
                daily_results[f'Breach_{p_name}'] = 0
                continue

            # Симулируем стоимость портфеля
            sim_values = simulate_portfolio_value(None, None, None, models_coef, factor_paths_h1, p_config, previous_day_prices, SHOW_PROGRESS=False)
            
            # Оцениваем риски с помощью обновленной функции
            var_99, es, _ = estimate_risk(sim_values, initial_value)

            # --- 3. Расчет фактического убытка ---
            current_day_prices = all_data.loc[current_date]
            
            # Стоимость портфеля на конец текущего дня
            current_day_value = 0
            for asset_type, assets in p_config.items():
                for ticker, rub_value in assets.items():
                    # Предполагаем, что состав портфеля в рублях фиксирован на prev_trade_date
                    units = rub_value / previous_day_prices.get(ticker, 1) # get() для безопасности
                    current_day_value += units * current_day_prices.get(ticker, 1)

            actual_return = (current_day_value - initial_value) / initial_value
            actual_loss = -actual_return

            # --- 4. Сохранение результатов ---
            daily_results[f'VaR_{p_name}'] = var_99
            daily_results[f'ES_{p_name}'] = es
            daily_results[f'Loss_{p_name}'] = actual_loss
            daily_results[f'Breach_{p_name}'] = 1 if actual_loss > var_99 else 0
        
        backtest_results.append(daily_results)
        
    return pd.DataFrame(backtest_results).set_index('date')
In [42]:
# Определим состав портфеля в рублях (как в вашем коде)
portfolio_positions_rub = {
    'stocks': {
        'SBER': 1e6, 'YDEX': 1e6, 'ROSN': 1e6, 'PLZL': 1e6, 'LKOH': 1e6,
        'GAZP': 1e6, 'NVTK': 1e6, 'MOEX': 1e6, 'CHMF': 1e6, 'GMKN': 1e6
    },
    'bonds': {
        'RU000A0JS3W6': 10e6, 'RU000A0ZYUA9': 10e6, 'RU000A100EF5': 10e6,
        'RU000A101QE0': 10e6, 'RU000A1028E3': 10e6
    },
    'fx': {
        'eur': 100e6, 'usd': 100e6
    }
}


# Запускаем бэктестинг на 2024 год
# ПРИМЕЧАНИЕ: Код может выполняться долго (от 30 минут до нескольких часов)
backtesting_df = perform_var_backtesting(
    all_data=all_data,
    prices_stocks=stocks,
    prices_bonds=bond_prices,
    prices_fx=df_index[['eur', 'usd']],
    all_data_diff=all_data_diff,
    pca_diff=pca_diff,
    mle_estimation=mle_estimation,
    portfolio_positions_rub=portfolio_positions_rub,
    backtest_year=2024,
    n_simulations=2000,
    horizont_simulation = 10
)
Бэктестинг VaR за 2024 год:   0%|          | 0/261 [00:00<?, ?it/s]

Тест Купиеца (Kupiec, 1995) – Тест безусловного покрытия (Unconditional Coverage - UC)¶

Описание: Тест Купиеца, также известный как тест "Proportion of Failures" (POF), проверяет, соответствует ли наблюдаемая доля нарушений VaR (Value-at-Risk) теоретической вероятности нарушений, заданной уровнем доверия VaR. Он оценивает, является ли количество фактических превышений VaR статистически совместимым с тем количеством, которое ожидается при правильной модели. Основное предположение теста Купиеца состоит в том, что каждое нарушение VaR является независимым событием, которое следует распределению Бернулли.

Гипотезы:

  • $H_0$: Наблюдаемая доля нарушений равна ожидаемой вероятности нарушений ($p$).
  • $H_1$: Наблюдаемая доля нарушений не равна ожидаемой вероятности нарушений ($p$).

Формула: Статистика теста Купиеца представляет собой отношение правдоподобия (Likelihood Ratio - LR), которое асимптотически распределено как $\chi^2$ с 1 степенью свободы:

$$LR_{UC} = -2 \ln \left( \frac{(1-p)^{N-x} p^x}{(1 - x/N)^{N-x} (x/N)^x} \right)$$

Где:

  • $N$ – общее количество наблюдений (например, количество дней).
  • $x$ – количество наблюдаемых нарушений VaR (число случаев, когда фактический убыток превысил VaR).
  • $p$ – ожидаемая вероятность нарушения VaR (например, для 99% VaR, $p = 1 - 0.99 = 0.01$).
In [43]:
# Тест Kupiec'а
def kupiec_pof_test(T, N, p_star=0.01, confidence_level=0.95):
    """
    Проводит тест Купика на долю отказов (POF-test).
    
    Parameters
    ----------
    T : int
        Общее количество наблюдений.
    N : int
        Количество пробоев VaR.
    p_star : float, default=0.01
        Ожидаемая доля пробоев (1% для 99% VaR).
    confidence_level : float, default=0.95
        Уровень доверия для определения критического значения.

    Returns
    -------
    lr_statistic : float
        Значение LR-статистики.
    p_value : float
        P-значение теста.
    result : str
        Вывод о принятии или отвержении гипотезы H0.
    """
    if N == 0 or N == T:
        return np.nan, np.nan, "Невозможно рассчитать (0 или 100% пробоев)"

    p_hat = N / T
    
    # Формула статистики отношения правдоподобия
    log_likelihood_h1 = (T - N) * np.log(1 - p_hat) + N * np.log(p_hat)
    log_likelihood_h0 = (T - N) * np.log(1 - p_star) + N * np.log(p_star)
    
    lr_statistic = -2 * (log_likelihood_h0 - log_likelihood_h1)
    
    # p-value рассчитывается из хи-квадрат распределения с 1 степенью свободы
    p_value = 1 - chi2.cdf(lr_statistic, 1)
    
    critical_value = chi2.ppf(confidence_level, 1)
    
    if lr_statistic > critical_value:
        result = f"Отвергнуть H0 (статистика {lr_statistic:.2f} > {critical_value:.2f})"
    else:
        result = f"Не отвергать H0 (статистика {lr_statistic:.2f} <= {critical_value:.2f})"
        
    return lr_statistic, p_value, result
In [44]:
print("Результаты бэктестинга (первые 5 дней 2024 г.):")
display(backtesting_df.head())

# Проверка гипотезы
T = len(backtesting_df)
expected_breaches = T * 0.01
summary_data = []

portfolio_map = {
    'total': 'Весь портфель',
    'stocks': 'Акции',
    'bonds': 'Облигации',
    'fx': 'Валюта'
}

for p_key, p_name in portfolio_map.items():
    N = backtesting_df[f'Breach_{p_key}'].sum()
    lr_stat, p_val, conclusion = kupiec_pof_test(T, N, p_star=0.01)
    summary_data.append({
        'Портфель': p_name,
        'Торговых дней (T)': T,
        'Ожидалось пробоев (1%)': f"{expected_breaches:.2f}",
        'Факт. пробоев (N)': N,
        'Доля пробоев': f"{(N/T)*100:.2f}%",
        'LR-статистика': f"{lr_stat:.3f}" if not np.isnan(lr_stat) else "n/a",
        'p-value': f"{p_val:.3f}" if not np.isnan(p_val) else "n/a",
        'Вывод (alpha=5%)': conclusion
    })

summary_df = pd.DataFrame(summary_data)
print("\nИтоговая таблица валидации VaR 99% за 2024 год:")
display(summary_df)
Результаты бэктестинга (первые 5 дней 2024 г.):
VaR_total ES_total Loss_total Breach_total VaR_stocks ES_stocks Loss_stocks Breach_stocks VaR_bonds ES_bonds Loss_bonds Breach_bonds VaR_fx ES_fx Loss_fx Breach_fx
date
2024-01-02 0.012316 0.012454 -0.000000 0 0.024205 0.024166 -0.000000 0 0.002309 0.002279 -0.000000 0 0.016204 0.016377 -0.0 0
2024-01-03 0.013584 0.013310 -0.000942 0 0.025869 0.025959 -0.015581 0 0.002455 0.002439 -0.001785 0 0.017852 0.017401 -0.0 0
2024-01-04 0.012671 0.012659 0.000212 0 0.026149 0.025497 0.003479 0 0.002218 0.002229 0.000404 0 0.016850 0.016492 -0.0 0
2024-01-05 0.011801 0.012146 0.000040 0 0.023600 0.024727 -0.001788 0 0.002143 0.002119 0.000566 0 0.015683 0.015939 -0.0 0
2024-01-08 0.012482 0.012633 -0.000377 0 0.024507 0.024542 -0.006999 0 0.001968 0.001996 -0.000559 0 0.016353 0.016535 -0.0 0
Итоговая таблица валидации VaR 99% за 2024 год:
Портфель Торговых дней (T) Ожидалось пробоев (1%) Факт. пробоев (N) Доля пробоев LR-статистика p-value Вывод (alpha=5%)
0 Весь портфель 261 2.61 6 2.30% 3.254 0.071 Не отвергать H0 (статистика 3.25 <= 3.84)
1 Акции 261 2.61 12 4.60% 18.179 0.000 Отвергнуть H0 (статистика 18.18 > 3.84)
2 Облигации 261 2.61 70 26.82% 345.041 0.000 Отвергнуть H0 (статистика 345.04 > 3.84)
3 Валюта 261 2.61 7 2.68% 5.107 0.024 Отвергнуть H0 (статистика 5.11 > 3.84)

Тест Кристофферсена (Christoffersen, 1998) – Тест условного покрытия (Conditional Coverage - CC)¶

Описание: Тест Кристофферсена расширяет тест Купиеца, добавляя проверку независимости нарушений VaR. Он проверяет не только правильную частоту нарушений (безусловное покрытие), но и то, являются ли эти нарушения независимыми друг от друга во времени. Это важно, поскольку модель VaR может показывать правильное общее количество нарушений, но если эти нарушения имеют тенденцию группироваться (кластеризоваться), это указывает на то, что модель неадекватно улавливает динамику риска. Тест Кристофферсена разбивается на две части: тест безусловного покрытия (который идентичен тесту Купиеца) и тест независимости.

Гипотезы: Тест Кристофферсена проверяет объединенную гипотезу:

  • $H_0$: Модель VaR обладает как правильным безусловным покрытием, так и независимыми нарушениями.
  • $H_1$: Модель VaR не удовлетворяет одному или обоим этим условиям.

Статистика теста Кристофферсена также является отношением правдоподобия и имеет асимптотическое распределение $\chi^2$ с 2 степенями свободы:

$$LR_{CC} = LR_{UC} + LR_{IND}$$

Где $LR_{UC}$ — это статистика теста Купиеца, а $LR_{IND}$ — статистика теста независимости.

Формула для теста независимости ($LR_{IND}$): Для расчета $LR_{IND}$ необходимо определить переходные вероятности Марковской цепи первого порядка для последовательности нарушений. Определим $I_t$ как индикаторную переменную, равную 1, если произошло нарушение VaR в момент $t$, и 0 в противном случае.

Определим следующие частоты:

  • $N_{00}$: Количество дней, когда не было нарушения, за которым не последовало нарушение.
  • $N_{01}$: Количество дней, когда не было нарушения, за которым последовало нарушение.
  • $N_{10}$: Количество дней, когда было нарушение, за которым не последовало нарушение.
  • $N_{11}$: Количество дней, когда было нарушение, за которым последовало нарушение.

Оценочные вероятности перехода:

  • $\pi_{01} = \frac{N_{01}}{N_{00} + N_{01}}$ (вероятность нарушения, если не было нарушения в предыдущий день)
  • $\pi_{11} = \frac{N_{11}}{N_{10} + N_{11}}$ (вероятность нарушения, если было нарушение в предыдущий день)

Также определим безусловную вероятность нарушения:

  • $\pi = \frac{N_{01} + N_{11}}{N_{00} + N_{01} + N_{10} + N_{11}}$ (что эквивалентно $x/N$ в тесте Купиеца).

Статистика $LR_{IND}$ рассчитывается как:

$$LR_{IND} = -2 \ln \left( \frac{(1-\pi)^{N_{00}+N_{10}} \pi^{N_{01}+N_{11}}}{(1-\pi_{01})^{N_{00}} \pi_{01}^{N_{01}} (1-\pi_{11})^{N_{10}} \pi_{11}^{N_{11}}} \right)$$

Эта статистика асимптотически распределена как $\chi^2$ с 1 степенью свободы.

Тест Хурлина и Топкави (Hurlin & Topkavi, 2007) – Двойной условный тест (Double Conditional Test - DCC)¶

Описание: Тест Хурлина и Топкави (2007) является расширением теста Кристофферсена и предлагает подход к тестированию условного покрытия, который учитывает не только зависимость от предыдущего нарушения, но и потенциальную зависимость от значения предыдущего убытка. Хотя их работа фокусируется на "двойном условном тесте", который учитывает как предыдущее состояние (нарушение/не нарушение), так и размер убытка, для целей простого описания с формулами, мы можем рассмотреть его как дальнейшее усложнение идеи условного покрытия.

К сожалению, явная и общепринятая формула "теста Хурлина и Топкави (2007)" как единой, компактной LR-статистики, аналогичной Купиецу или Кристофферсену, не так широко цитируется. Их работа скорее предлагает более общий фреймворк для тестирования динамических свойств моделей VaR, выходящий за рамки простой бинарной последовательности нарушений. Они обсуждают "динамический тест точности" (Dynamic Accuracy Test) и "динамический тест независимости" (Dynamic Independence Test), которые могут включать дополнительные информационные переменные.

В общем, их подход направлен на то, чтобы проверить гипотезы вида $P(I_t = 1 | \mathcal{F}_{t-1}) = p$, где $\mathcal{F}_{t-1}$ - это информационный набор, доступный в $t-1$, который может включать не только $I_{t-1}$, но и другие переменные, такие как предыдущие значения VaR, доходности или убытки.

Концептуально, основные идеи, лежащие в основе подхода Hurlin & Topkavi (2007), включают:

  1. Расширение информационного набора: Вместо того чтобы полагаться только на бинарную последовательность нарушений ($I_t$), они предлагают включать в условную вероятность другие переменные, которые могут влиять на будущие нарушения (например, предыдущие доходности, волатильность, размер предыдущего убытка).
  2. Динамические модели для вероятности нарушения: Вероятность нарушения $p_t = P(I_t = 1 | \mathcal{F}_{t-1})$ не обязательно является константой $p$, а может быть динамической и зависеть от $\mathcal{F}_{t-1}$.
  3. Тестирование специфических форм зависимости: Их тесты могут быть направлены на выявление конкретных типов зависимости, которые не улавливаются простыми марковскими цепями первого порядка.

Хотя прямая "формула" для всего их теста в виде одного LR-статистика отсутствует, общая идея заключается в использовании моделей, таких как логит или пробит, для моделирования вероятности нарушения $I_t$ как функции от предыдущих значений и затем тестирования коэффициентов этих моделей.

Например, если бы мы хотели смоделировать $P(I_t=1|\mathcal{F}_{t-1})$ с учетом предыдущего значения $I_{t-1}$ и $r_{t-1}$ (предыдущей доходности), мы могли бы использовать логит-модель:

$$P(I_t=1|\mathcal{F}_{t-1}) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 I_{t-1} + \beta_2 r_{t-1})}}$$

Тогда тест на независимость и условное покрытие будет заключаться в проверке гипотез о коэффициентах ($\beta_1$, $\beta_2$ и т.д.).

В целом, тест Hurlin & Topkavi (2007) не предоставляет единой универсальной формулы LR-статистики, как Kupiec или Christoffersen, а скорее набор методологий для более продвинутого анализа условного покрытия, включающего в себя более богатый информационный набор и динамические модели.

In [45]:
import numpy as np
import pandas as pd
from scipy.stats import chi2
from scipy.optimize import minimize

# --- Тест Кристофферсена ---

def christoffersen_test(breaches: pd.Series, p_star: float = 0.01):
    """
    Проводит тест Кристофферсена на условное покрытие (частота + независимость).

    Parameters
    ----------
    breaches : pd.Series
        Временной ряд из 0 (нет пробоя) и 1 (есть пробой).
    p_star : float, default=0.01
        Ожидаемая доля пробоев.

    Returns
    -------
    dict
        Словарь со статистиками, p-value и выводами для каждого компонента теста.
    """
    n = len(breaches)
    n1 = breaches.sum()
    n0 = n - n1
    
    if n1 < 2:
        return {
            'LR_uc': np.nan, 'p_val_uc': np.nan, 'result_uc': 'Слишком мало пробоев',
            'LR_ind': np.nan, 'p_val_ind': np.nan, 'result_ind': 'Слишком мало пробоев',
            'LR_cc': np.nan, 'p_val_cc': np.nan, 'result_cc': 'Слишком мало пробоев',
        }

    # 1. Тест на безусловное покрытие (Купик)
    lr_uc, p_val_uc, res_uc = kupiec_pof_test(n, n1, p_star)

    # 2. Тест на независимость
    n01, n11, n00, n10 = 0, 0, 0, 0
    for i in range(1, n):
        if breaches.iloc[i-1] == 0 and breaches.iloc[i] == 1: n01 += 1
        if breaches.iloc[i-1] == 1 and breaches.iloc[i] == 1: n11 += 1
        if breaches.iloc[i-1] == 0 and breaches.iloc[i] == 0: n00 += 1
        if breaches.iloc[i-1] == 1 and breaches.iloc[i] == 0: n10 += 1
    
    pi0 = (n01) / (n00 + n01) if (n00 + n01) > 0 else 0
    pi1 = (n11) / (n10 + n11) if (n10 + n11) > 0 else 0
    pi = (n01 + n11) / n

    if pi < 1e-9 or pi0 < 1e-9 or pi1 < 1e-9: #
         lr_ind = np.nan
    else:
        log_L0 = (n00 + n10) * np.log(1 - pi) + (n01 + n11) * np.log(pi)
        log_L1 = n00 * np.log(1 - pi0) + n01 * np.log(pi0) + n10 * np.log(1 - pi1) + n11 * np.log(pi1)
        lr_ind = -2 * (log_L0 - log_L1)

    p_val_ind = 1 - chi2.cdf(lr_ind, 1)
    res_ind = "Отвергнуть H0 (Кластеризация)" if p_val_ind < 0.05 else "Не отвергать H0"

    # 3. Совмещенный тест
    lr_cc = lr_uc + lr_ind
    p_val_cc = 1 - chi2.cdf(lr_cc, 2)
    res_cc = "Отвергнуть H0" if p_val_cc < 0.05 else "Не отвергать H0"

    return {
        'LR_uc': lr_uc, 'p_val_uc': p_val_uc, 'result_uc': res_uc.split(' ')[0],
        'LR_ind': lr_ind, 'p_val_ind': p_val_ind, 'result_ind': res_ind,
        'LR_cc': lr_cc, 'p_val_cc': p_val_cc, 'result_cc': res_cc,
    }

# --- Тест на основе длительности (Candelon et al.) ---

def duration_based_test_candelon(breaches: pd.Series):
    """
    Проводит тест на независимость пробоев на основе длительности между ними.
    (Candelon, Colletaz, Hurlin, Tokpavi, 2011)
    """
    breach_indices = breaches[breaches == 1].index
    if len(breach_indices) < 2:
        return np.nan, np.nan, "Слишком мало пробоев"

    # Рассчитываем длительности
    first_breach_day_number = breaches.index.get_loc(breach_indices[0]) + 1
    durations = np.diff(breaches.index.get_indexer_for(breach_indices))
    durations = np.insert(durations, 0, first_breach_day_number)
    
    # Логарифмическая функция правдоподобия для распределения Вейбулла
    def log_likelihood_weibull(params, D):
        b, c = params[0], params[1]
        if b <= 0 or c <= 0: return -np.inf
        # Формула из статьи Candelon et al.
        ll = -np.sum( (D/b)**c - np.log(c * D**(c-1) / b**c) )
        return ll

    # Оптимизация для неограниченной модели (c != 1)
    res_unrestricted = minimize(
        lambda p: -log_likelihood_weibull(p, durations),
        x0=[np.mean(durations), 1.0], method='Nelder-Mead'
    )
    ll_unrestricted = -res_unrestricted.fun

    # Оптимизация для ограниченной модели (c = 1, экспоненциальное распределение)
    res_restricted = minimize(
        lambda p: -log_likelihood_weibull([p[0], 1.0], durations),
        x0=[np.mean(durations)], method='Nelder-Mead'
    )
    ll_restricted = -res_restricted.fun

    # Статистика отношения правдоподобия
    lr_dur = 2 * (ll_unrestricted - ll_restricted)
    p_value = 1 - chi2.cdf(lr_dur, 1)
    result = "Отвергнуть H0 (Есть память)" if p_value < 0.05 else "Не отвергать H0"
    
    return lr_dur, p_value, result
In [46]:
T = len(backtesting_df)
expected_breaches = T * 0.01
summary_data = []

portfolio_map = {
    'total': 'Весь портфель',
    'stocks': 'Акции',
    'bonds': 'Облигации',
    'fx': 'Валюта'
}

for p_key, p_name in portfolio_map.items():
    N = backtesting_df[f'Breach_{p_key}'].sum()
    breach_series = backtesting_df[f'Breach_{p_key}']
    
    # Выполняем все тесты
    kupiec_res = kupiec_pof_test(T, N, p_star=0.01)
    chris_res = christoffersen_test(breach_series, p_star=0.01)
    dur_res = duration_based_test_candelon(breach_series)
    
    summary_data.append({
        'Портфель': p_name,
        'Дней (T)': T,
        'Ожидалось пробоев (1%)': f"{expected_breaches:.2f}",
        'Факт. пробоев (N)': N,
        # Тест Купика (Безусловное покрытие)
        'Kupiec (p-val)': f"{kupiec_res[1]:.3f}" if not np.isnan(kupiec_res[1]) else "n/a",
        'Kupiec (Вывод)': kupiec_res[2].split(' ')[0],
        # Тест Кристофферсена на независимость
        'Chris. Indep (p-val)': f"{chris_res['p_val_ind']:.3f}" if not np.isnan(chris_res['p_val_ind']) else "n/a",
        'Chris. Indep (Вывод)': chris_res['result_ind'],
        # Полный тест Кристофферсена
        'Chris. CC (p-val)': f"{chris_res['p_val_cc']:.3f}" if not np.isnan(chris_res['p_val_cc']) else "n/a",
        'Chris. CC (Вывод)': chris_res['result_cc'],
        # Тест на основе длительности
        'Duration Test (p-val)': f"{dur_res[1]:.3f}" if not np.isnan(dur_res[1]) else "n/a",
        'Duration Test (Вывод)': dur_res[2],
    })

summary_df = pd.DataFrame(summary_data).set_index('Портфель')
print("\nИтоговая таблица валидации VaR 99% за 2024 год (включая расширенные тесты):")
display(summary_df)
Итоговая таблица валидации VaR 99% за 2024 год (включая расширенные тесты):
Дней (T) Ожидалось пробоев (1%) Факт. пробоев (N) Kupiec (p-val) Kupiec (Вывод) Chris. Indep (p-val) Chris. Indep (Вывод) Chris. CC (p-val) Chris. CC (Вывод) Duration Test (p-val) Duration Test (Вывод)
Портфель
Весь портфель 261 2.61 6 0.071 Не 0.004 Отвергнуть H0 (Кластеризация) 0.003 Отвергнуть H0 0.108 Не отвергать H0
Акции 261 2.61 12 0.000 Отвергнуть 0.105 Не отвергать H0 0.000 Отвергнуть H0 0.172 Не отвергать H0
Облигации 261 2.61 70 0.000 Отвергнуть 0.000 Отвергнуть H0 (Кластеризация) 0.000 Отвергнуть H0 0.777 Не отвергать H0
Валюта 261 2.61 7 0.024 Отвергнуть 0.166 Не отвергать H0 0.030 Отвергнуть H0 0.087 Не отвергать H0
In [47]:
csv_path = "backtesting_summary_2024_2000_10.csv"
summary_df.to_csv(csv_path, index=True, encoding="utf-8-sig")
In [32]:
import pandas as pd

print('Результаты при запуске 2000 траекторий')
display(pd.read_csv('data/results/backtesting_summary_2024_2000_10.csv'))
print('\nРезультаты при запуске 10000 траекторий')
display(pd.read_csv('data/results/backtesting_summary_2024_10000_10.csv'))
Результаты при запуске 2000 траекторий
Портфель Дней (T) Ожидалось пробоев (1%) Факт. пробоев (N) Kupiec (p-val) Kupiec (Вывод) Chris. Indep (p-val) Chris. Indep (Вывод) Chris. CC (p-val) Chris. CC (Вывод) Duration Test (p-val) Duration Test (Вывод)
0 Весь портфель 261 2.61 6 0.071 Не 0.004 Отвергнуть H0 (Кластеризация) 0.003 Отвергнуть H0 0.108 Не отвергать H0
1 Акции 261 2.61 12 0.000 Отвергнуть 0.105 Не отвергать H0 0.000 Отвергнуть H0 0.172 Не отвергать H0
2 Облигации 261 2.61 70 0.000 Отвергнуть 0.000 Отвергнуть H0 (Кластеризация) 0.000 Отвергнуть H0 0.777 Не отвергать H0
3 Валюта 261 2.61 7 0.024 Отвергнуть 0.166 Не отвергать H0 0.030 Отвергнуть H0 0.087 Не отвергать H0
Результаты при запуске 10000 траекторий
Портфель Дней (T) Ожидалось пробоев (1%) Факт. пробоев (N) Kupiec (p-val) Kupiec (Вывод) Chris. Indep (p-val) Chris. Indep (Вывод) Chris. CC (p-val) Chris. CC (Вывод) Duration Test (p-val) Duration Test (Вывод)
0 Весь портфель 261 2.61 6 0.071 Не 0.004 Отвергнуть H0 (Кластеризация) 0.003 Отвергнуть H0 0.108 Не отвергать H0
1 Акции 261 2.61 11 0.000 Отвергнуть 0.072 Не отвергать H0 0.000 Отвергнуть H0 0.244 Не отвергать H0
2 Облигации 261 2.61 70 0.000 Отвергнуть 0.000 Отвергнуть H0 (Кластеризация) 0.000 Отвергнуть H0 0.777 Не отвергать H0
3 Валюта 261 2.61 6 0.071 Не 0.114 Не отвергать H0 0.056 Не отвергать H0 0.161 Не отвергать H0